93 Commits
Author SHA1 Message Date
Neko Ayaka 349ecfeda2 release: v0.14.1 2026-10-30 23:42:06 +08:00
Neko Ayaka c32506cd2b release: v0.14.0 2026-10-30 02:27:22 +08:00
Neko Ayaka 5d97ac41b7 release: v0.13.1 2026-10-29 07:19:40 +08:00
215cfc3e64 feat(api-server): send configured headers to LLM upstreams (#3200)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Neko <neko@ayaka.moe>
2026-10-29 02:48:37 +08:00
jim139129 32054c07cf fix(security): update runtimes for CVE-2026-111497 (#3184) 2026-10-28 20:45:56 +08:00
Neko Ayaka 3c4aca2093 release: v0.13.0 2026-10-28 05:26:59 +08:00
Neko Ayaka 3b0672e366 release: v0.12.0 2026-10-23 18:58:29 +08:00
Neko Ayaka c7c9c91791 release: v0.12.0-beta.6 2026-10-23 14:31:31 +08:00
Younsang Na de5844c3fa test: run workspace tests through Turbo (#3048) 2026-10-21 20:44:37 +08:00
RainbowBirdandClaude Opus 5.5 7a0f5bc391 fix(api,stage-ui): keep the character of synchronized chats (#2874)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 01:56:16 +08:00
RainbowBirdandClaude Opus 5.5 b7ca814039 feat: show account Notifications from AIRI Cloud (#3022)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 10:53:40 +00:00
Neko 8781d10cd9 refactor(server): use valibot format actions instead of regexes (#3045) 2026-10-20 00:27:40 +08:00
Neko 80cf17f344 fix(server): match trusted origins by their parsed URL (#3043) 2026-10-19 23:54:52 +08:00
Neko 06103df976 feat(api-server): pass the language of speech text to StepFun (#3042) 2026-10-19 23:54:22 +08:00
RainbowBirdandClaude Opus 5.5 c0f961e234 docs(server): define HTTP response conventions (#3028)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 17:15:33 +08:00
RainbowBirdandClaude Opus 5.5 4608dea156 feat(api): add Redis Streams infrastructure and publish account events (#3025)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:05:06 +08:00
RainbowBird 7c6bb0df93 feat(capacitor): show quota and refill details (#3004) 2026-10-17 10:33:25 +00:00
railway-app[bot] 6b1cb3b257 fix: import errors from http-error-shared (#3005) 2026-10-17 18:16:59 +08:00
RainbowBirdandClaude Opus 5.5 9245a5b69b refactor(server): share one error model between the API and Auth services (#2881)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 17:34:37 +08:00
蓝莓🫐 63cc8c61b1 test(api): fix the clock of the RevenueCat upgrade test (#2982) 2026-10-15 16:48:30 +08:00
RainbowBirdandClaude Opus 5.5 831d07811e fix(api): grant only the RevenueCat subscriptions of the deployment environment (#2970)
## Summary

RevenueCat returns the subscriptions of both store environments,
`production` and `sandbox`. The API granted Capacitor Flux for both.

The API now grants only the subscriptions of its deployment environment.
`REVENUECAT_ENVIRONMENT` is `production` by default.
`REVENUECAT_SANDBOX_USER_IDS` lists dedicated test accounts whose
sandbox subscriptions production also grants. This follows
`APPLE_IAP_SANDBOX_USER_IDS`.

This PR is independent.

## Architecture and behavior

The filter is in `createRevenuecatSubscriberClient`
(`server/apps/api/src/services/adapters/revenuecat-subscriber.ts`). The
Capacitor reconciliation (`fetchEntitlements`) and the account deletion
check (`hasRenewingSubscription`) both use it.

```mermaid
sequenceDiagram
  participant Caller as Webhook, sync, or deletion check
  participant Reader as revenuecat-subscriber.ts
  participant RC as RevenueCat API v2
  Caller->>Reader: fetchEntitlements or hasRenewingSubscription
  Reader->>RC: GET customers/{id}/subscriptions
  RC-->>Reader: subscriptions with environment
  alt environment matches REVENUECAT_ENVIRONMENT, or sandbox for a listed user
    Reader-->>Caller: the subscription counts
  else another environment
    Reader-->>Caller: the subscription is skipped
  end
```

`GET customers/{id}/active_entitlements` ignores the `environment`
parameter. When a skipped subscription grants the same entitlement, the
access ends at `current_period_ends_at` of the subscription that counts.
Otherwise the active entitlement keeps its end, so a grace period still
counts.

## Boundaries and risks

| Invariant or boundary | Failure mode | Protection | Evidence or gap |
| --- | --- | --- | --- |
| Production grants only production subscriptions | A sandbox
subscription grants Capacitor Flux | `countsFor` in `fetchEntitlements`
| `grants no sandbox subscription to a production user` |
| Test accounts and sandbox deployments still work | The filter blocks
testing | `REVENUECAT_SANDBOX_USER_IDS`,
`REVENUECAT_ENVIRONMENT=sandbox` | `grants a sandbox subscription to a
listed test account and in a sandbox deployment` |
| A skipped subscription cannot extend access | The shared active
entitlement takes the later end | `current_period_ends_at` of the
counted subscription | `ends the access with the paid period when a
sandbox subscription shares the entitlement` |
| A sandbox subscription does not block deletion | A deletion is refused
| `countsFor` in `hasRenewingSubscription` | `is false for a renewing
sandbox subscription of a production user` |

## Verification

```text
pnpm -F @proj-airi/api-server typecheck          # passed
pnpm -F @proj-airi/api-server exec vitest run    # 82 files passed, 2 skipped. 884 tests passed, 3 skipped
pnpm typecheck                                   # passed before the rebase on main
pnpm lint                                        # 0 errors before the rebase on main
```

The four new tests fail on `main` and pass with this change.

Not verified: a deployment with the new variables.

## Rollout and follow-up

- Production needs no new variable. The default is `production`.
- The `dev` `ts-api` service already has
`REVENUECAT_ENVIRONMENT=sandbox`.
- Merge this PR before the Capacitor product change branch. Both change
`revenuecat-subscriber.ts`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:26:08 +08:00
RainbowBirdandClaude Sonnet 5.5 dfb59461d3 feat(api,stage-ui): sync Live2D and VRM models to private storage (#2869)
## Summary

Imported Live2D ZIP and VRM models live only in the browser IndexedDB,
so they do not reach other devices. This PR adds private per-account
model sync: model metadata in Postgres, original files in S3, and device
caches in IndexedDB.

Cloud sync is behind the new local `display-model-sync` feature flag. It
is off by default.

Object storage is now required. The API does not start without
`S3_BUCKET` and `S3_REGION`. **Set them in every deployed environment
before merge.** The local compose stack provides RustFS.

The upload logic is generic (`upload_sessions`, `UploadSessionService`,
`TransferQueue`), so other file types can reuse it. The ADR is
`server/docs/ai/adr/2026-10-08-display-model-cloud-sync.md`. Its status
is still `proposed`. It records that a local file is a device working
copy and an uploaded file is an account asset.

This PR is independent.

## Change map

| Module | Before | After | Description |
| --- | --- | --- | --- |
| `object-store` adapter | Signs PUT with content type and metadata |
Also signs `x-amz-checksum-sha256` and `If-None-Match: *`. Reads the
stored checksum and supports ranged GET | The store rejects wrong bytes
and second writes |
| `upload-sessions` (new) | None | Server-chosen key, idempotent
reservation, size, checksum and signature check | Generic. The business
domain owns meaning and quota |
| `display-models` domain and routes (new) | None | List, reserve,
complete, download, rename, delete under `/api/v1/display-models` |
Revision checks. Delete writes a tombstone |
| Drizzle `0033` | None | `display_models` and `upload_sessions` | |
| `libs/file-transfer` (new) | None | Hash, signed PUT, verified
download, durable account-scoped queue | Resumes after restart. Drops
results after account change |
| Device storage for models | `localforage` in the store |
`database/repos/display-models.repo.ts`, still on `localforage` with the
same keys | No local data moves. The `unstorage` move is a separate pull
request |
| S3 configuration | Optional | Required at startup | Same class of
dependency as Redis |
| Local compose stack | No object storage | RustFS, a bucket, and CORS
for local web origins | `s3.localhost` resolves inside the network and
in a browser |
| `stores/display-models` | Local files only | Cloud directory, sync
status, upload queue, download on demand | Gated by the feature flag |
| `stores/settings/stage-model` | Missing model falls back to default |
A failed cloud download keeps the selection | Local-missing models still
fall back |
| Model selector | Local list | Cloud-only entries, status, upload and
delete actions | |

## Architecture and behavior

```mermaid
sequenceDiagram
  participant A as Device A
  participant API as API
  participant DB as Postgres
  participant S3 as Private S3
  participant B as Device B
  A->>A: Import and save File locally
  A->>API: POST /uploads (id, requestId, size, sha256)
  API->>DB: Lock owner, check quota, insert session
  API-->>A: Signed PUT (checksum, write-once)
  A->>S3: PUT bytes
  A->>API: POST /uploads/:id/complete
  API->>S3: HEAD checksum and size, ranged GET signature
  alt mismatch
    API->>S3: Delete object, abort session (422)
  else match
    API->>DB: Publish ready model
  end
  B->>API: GET /display-models
  B->>API: POST /:id/download
  B->>S3: GET, verify size and sha256, cache locally
```

## Boundaries and risks

| Invariant or boundary | Failure mode | Protection | Evidence or gap |
| --- | --- | --- | --- |
| Only verified bytes become ready | Client uploads other bytes |
Store-validated SHA-256, size and signature check | Unit tests with a
fake store |
| Ready bytes never change | Reused signed URL overwrites |
`If-None-Match: *` and a per-upload key | Integration test passes
against RustFS: wrong bytes get 400, a second write gets 412. Production
provider unverified |
| Quota holds under races | Parallel reservations exceed it | Advisory
lock per owner in one transaction | Sequential test only. No concurrent
test |
| Accounts stay isolated | Another account reads a model | Every query
filters by owner | Tests for upload, download and list |
| Account switch is safe | Stale upload result lands in new account |
Queue generation check | Queue unit test |
| Deleted models leave devices | Cache stays | Tombstone removes the
cache on next sync | Not tested |

Not in this PR: physical S3 cleanup after delete, Live2D package
validation, card reference check on delete, stage-pocket acceptance.

## Visual changes

The model selector gains sync status and menu items. They show only when
the flag is on, the user is signed in, and the API has S3. I could not
capture before and after images without that backend. This is
unverified.

## Verification

- `display-models.repo.browser.test.ts` runs in CI Chromium. It reads a
4 MiB record in the previous release format, and keeps a model and an
upload across repository instances.

Run after the rebase onto `main`:

- `tsc --noEmit` in `server/apps/api`: no errors.
- `vitest run server/apps/api`: 78 files pass, 2 skipped.
- `vue-tsc --noEmit` in `stage-ui`: no errors.
- `vitest run --project node` for the model repository, model store,
`settings`, `file-transfer`, and feature flags in `stage-ui`: 23 tests
pass.
- `eslint` on changed files: no errors.

Run before the rebase, with no later change to that code:

- `object-store.integration.test.ts` against local RustFS
(`TEST_S3_ENDPOINT`): 2 tests pass.
- CORS preflight from `http://localhost:5173` for a signed PUT: allowed.

Not run:

- `pnpm lint` and the root `pnpm typecheck`.
- `vue-tsc` for `stage-web` and `stage-tamagotchi` after the storage
change.
- The full compose stack. Only RustFS was started.
- Real production S3, browser or Electron end-to-end, and the two-device
flow.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 16:06:15 +08:00
RainbowBirdandClaude Opus 5.5 0e64578ab1 fix(api,auth,stage-ui): close the Capacitor follow-ups of #2813 (#2910)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:09:26 +08:00
f677923036 feat(api,stage-ui): add the Capacitor, a subscription store of Flux (#2813)
Co-authored-by: RainbowBird <git@luoling.moe>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 01:18:20 +08:00
Neko b6cd1e97c8 fix(api,stage-ui): correct character card sync defects from #2817 (#2850) 2026-10-07 22:39:30 +08:00
RainbowBirdandClaude Sonnet 5.5 60d73ccd52 fix(api): return jsonb strings as stored in character card fields (#2840)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 03:48:37 +08:00
3848d155f9 feat(api,stage-ui): synchronize character cards by field (#2817)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Neko Ayaka <neko@ayaka.moe>
2026-10-06 18:49:25 +00:00
RainbowBird aeadd9226a refactor(api): replace Langfuse export with request logs (#2798) 2026-10-06 17:34:42 +00:00
RainbowBird 87230a0d4f fix(api): stop writing TTS requests to llm_request_log (#2791) 2026-10-04 07:29:27 +00:00
RainbowBirdandClaude Sonnet 5.5 e20e28936f refactor(api): drop the request-log Flux column and the settlement archive (#2785)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 21:33:39 +00:00
RainbowBirdandClaude Sonnet 5.5 4158789d07 feat(api): store LLM request and error content (#2675)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 05:23:38 +08:00
RainbowBirdandClaude Sonnet 5.5 41335077ee refactor(api): store micro-Flux fees in flux_usage (#2783)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 21:03:55 +00:00
RainbowBirdandClaude Sonnet 5.5 450c5d81dc feat(api): add provider-cost Flux settlement (#2644)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 21:13:04 +08:00
RainbowBird 7f750c88d0 fix(auth): return API and verification browser visits to AIRI (#2733) 2026-10-02 06:56:44 +00:00
RainbowBird dc686d9697 fix(api-server): create Stripe customers for unbound users (#2715) 2026-09-29 23:51:16 +08:00
Makito 2feea5ad3d docs(AGENTS.md): perform housekeeping on agent-facing docs (#2678) 2026-09-28 01:24:04 +08:00
Hatsune Miku b4289d3401 fix(api): allow Apple sandbox purchases for dedicated test accounts (#2674)
## Summary

TestFlight purchases use Apple's Sandbox environment. A production-only
verifier rejects them before Flux settlement.

This PR lets the production API verify Sandbox purchases for dedicated
test accounts. `APPLE_IAP_SANDBOX_USER_IDS` defaults to empty. A
nonempty list enables the Sandbox verifier, and both settlement paths
check the account against that list. Ordinary accounts cannot receive
Sandbox Flux.

This is an independent server change based on
`07ed52e39dff4884cb3f30bfcad319bed0836e69`. It does not include the
separate iOS recovery patch.

## Change map

| Boundary | Before | After |
| --- | --- | --- |
| Apple verification | One environment for every trusted app | Optional
Production and Sandbox verifiers, with full SDK signature checks |
| Account authorization | Account-token ownership only | Token ownership
plus an exact user-ID allowlist for Sandbox grants |
| Settlement identity | Raw transaction ID | Sandbox bundle/environment
namespace; existing unique index prevents duplicate grants |
| Notifications | Envelope and transaction verified separately | Both
identities must also match before settlement |

## Architecture and behavior

Before:

```mermaid
flowchart LR
  C[Device or Apple notification] --> V[Production verifier]
  V -->|Sandbox rejected| E[Environment error]
  V -->|Production verified| A[Account token lookup]
  A --> P[Payment settlement]
```

After:

```mermaid
flowchart LR
  C[Device or Apple notification] --> V[Select app and enabled environment verifier]
  V -->|Verify signature and identity| A[Account token lookup]
  A -->|Production| P[Payment settlement]
  A -->|Sandbox| G[Dedicated user allowlist]
  G -->|Listed user| P
  G -->|Unlisted user| R[Device 403 or notification acknowledgement without credit]
```

Unverified claims select an SDK verifier only. They cannot authorize
settlement. A production deployment never selects Xcode verification.
Notification envelopes and inner transactions must have the same bundle
and environment.

Both Sandbox delivery paths use `sandbox:<bundleId>:<transactionId>`.
Payment CORE uses the existing `(processor, processor_order_id)` unique
index and `ON CONFLICT DO NOTHING` to credit each purchase once,
including concurrent deliveries. Production order IDs stay unchanged.
There are no schema or migration changes.

## Boundaries and risks

- This is account-level access control, not a separate wallet or
database. Test Flux enters the dedicated account's existing balance and
can consume real provider resources.
- The deployment owner confirmed no historical Sandbox orders and no old
Sandbox writers during rollout. New purchases use the namespaced ID from
the first settlement. Historical order compatibility is outside this
rollout.
- Sandbox-only deployments now also require the account allowlist to
grant Flux.
- A blocked device submission returns 403 for retry. A blocked
notification returns 200 without a grant. Configure the allowlist before
the purchase.
- Removing an ID requires an API restart and does not delete an existing
balance. Payment records retain their Sandbox environment in extras;
reporting must exclude test orders.
- Refund processing and general TestFlight/App Review account
provisioning are outside this PR.

## Verification

- Before implementation, five new route assertions failed: unrestricted
device/notification grants, missing Sandbox order namespace, and two
envelope identity mismatches.
- `pnpm -F @proj-airi/api-server exec vitest run src/routes/apple-iap
src/libs/tests/env.test.ts
src/services/domain/payment/tests/payment.test.ts` — **57 passed**.
- Payment tests cover Sandbox replay, concurrent delivery, distinct
bundles, and separation from Production transaction IDs.
- `pnpm -F @proj-airi/server-sdk-shared build` — passed.
- `pnpm -F @proj-airi/api-server typecheck` — passed.
- `pnpm exec moeru-lint <all changed files>` — passed with pre-existing
comment-length warnings.
- `git diff --check` — passed.
- `pnpm typecheck` — could not run across the monorepo: this server-only
sparse checkout excludes the `packages`, `apps`, and `docs` roots.
- `pnpm lint` — did not pass across the sparse checkout: excluded
package manifests cause unused-catalog errors; unrelated existing
findings are also present. Changed files pass separately.

The real Apple SDK tests reject unsigned JWS input. SDK-boundary doubles
cover successful environment selection and retryable failures. PGlite
tests cover settlement replay and namespace separation. Live Apple
transactions, deployment, and notification delivery are **not
verified**.

## Rollout

1. Merge and deploy while keeping `APPLE_IAP_ENV=production`.
2. Add the dedicated AIRI account's internal ID to
`APPLE_IAP_SANDBOX_USER_IDS`, then restart the API.
3. Confirm the bundle/App Store IDs and ConfigKV `APPLE_FLUX_PACKS`
mappings.
4. Configure the Sandbox Notifications V2 URL and install an iOS build
with the recovery fixes.
5. Check purchase credit once, retry recovery, duplicate notification
delivery, and rejection for an ordinary account.

No database migration is required. The API README contains the
procedure. `server/docs/ai/adr/2026-09-27-apple-iap-sandbox-accounts.md`
records the decision and boundaries. No production configuration was
changed by this PR.
2026-09-27 23:44:16 +08:00
RainbowBird 07ed52e39d feat(api): add extensible LLM request tracking (#2673) 2026-09-27 19:40:36 +08:00
RainbowBird 4bba665da9 feat(api-server): add optional S3 object storage (#2669) 2026-09-27 08:45:30 +00:00
Lulu 0fbd91c27b feat(stage-ui): sync user providers to a cloud replica (#2471) 2026-09-27 00:37:51 +08:00
Lulu fab2dbeafa feat(api): add Apple IAP payment channel backend (#2665) 2026-09-26 21:43:15 +08:00
RainbowBird 40d6ffd8df fix(stage-ui): restore voice input after microphone changes (#2645) 2026-09-24 02:15:24 +08:00
RainbowBird 308ee2b3aa chore(server): remove deprecated Railway configs (#2642) 2026-09-22 19:44:51 +08:00
9805c0a0f3 refactor(api): extract payment CORE to support other payment providers [2/2] (#2368)
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: RainbowBird <git@luoling.moe>
2026-09-20 14:52:48 +08:00
RainbowBird 62ef8676f6 fix(api): route web search through OpenRouter (#2604) 2026-09-19 21:37:47 +08:00
RainbowBird 542db6c89b fix(api): preserve Responses provider fields (#2601) 2026-09-19 12:28:59 +00:00
RainbowBird 41e48b12ce fix: handle interrupted Responses streams (#2596) 2026-09-19 17:12:53 +08:00
RainbowBird cfffa9b6f3 fix(api): recover completed OpenRouter Responses streams (#2593) 2026-09-19 01:46:46 +08:00
RainbowBird 70bcee06a3 feat(api): add stateless Responses gateway with Flux settlement (#2554) 2026-09-18 17:51:24 +08:00
RainbowBird 0040675e86 feat(server): group TTS billing history by chat round (#2491)
Signed-off-by: RainbowBird <git@luoling.moe>
2026-09-18 06:41:41 +00:00