@mycadet/gateway 0.12.11 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  2. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  3. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  4. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  5. package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
  6. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  7. package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  8. package/openapi.json +1 -1
  9. package/package.json +1 -1
  10. package/src/__tests__/actor-token-revocation.test.ts +41 -34
  11. package/src/__tests__/contact-store-mirror-op-missing.test.ts +33 -70
  12. package/src/__tests__/contacts-mirror-op-reporter.test.ts +33 -89
  13. package/src/__tests__/credential-requests.test.ts +4 -4
  14. package/src/__tests__/devices-mine.test.ts +187 -0
  15. package/src/__tests__/edge-auth.test.ts +1 -20
  16. package/src/__tests__/edge-guardian-auth.test.ts +1 -17
  17. package/src/__tests__/feature-flag-resolver.test.ts +2 -30
  18. package/src/__tests__/feature-flag-watcher-callback.test.ts +0 -22
  19. package/src/__tests__/feature-flags-route.test.ts +2 -191
  20. package/src/__tests__/guardian-bootstrap-lookups.test.ts +18 -25
  21. package/src/__tests__/guardian-integrity-reporter.test.ts +51 -82
  22. package/src/__tests__/guardian-integrity.test.ts +16 -20
  23. package/src/__tests__/handle-inbound-trust-verdict.test.ts +1 -14
  24. package/src/__tests__/ipc-feature-flag-routes.test.ts +0 -56
  25. package/src/__tests__/lum-2783-recovery-e2e.test.ts +0 -12
  26. package/src/__tests__/post-assistant-ready-deferred.test.ts +0 -18
  27. package/src/__tests__/refresh-device-binding.test.ts +0 -12
  28. package/src/__tests__/remote-web-pairing-token.test.ts +4 -14
  29. package/src/__tests__/runtime-client.test.ts +7 -7
  30. package/src/__tests__/speech-relay-websocket.test.ts +4 -4
  31. package/src/__tests__/trust-verdict-resolver-guardian-integrity.test.ts +30 -36
  32. package/src/__tests__/twilio-webhooks.test.ts +2 -2
  33. package/src/auth/actor-token-revocation.ts +34 -9
  34. package/src/cli/__tests__/pairing.test.ts +95 -0
  35. package/src/cli/index.ts +1 -1
  36. package/src/cli/pairing.ts +81 -6
  37. package/src/contacts-mirror-op-reporter.ts +19 -36
  38. package/src/db/connection.ts +0 -10
  39. package/src/feature-flag-resolver.ts +2 -7
  40. package/src/feature-flag-watcher.ts +1 -14
  41. package/src/guardian-integrity-reporter.ts +19 -47
  42. package/src/http/middleware/auth.ts +33 -27
  43. package/src/http/routes/auth-token.ts +1 -1
  44. package/src/http/routes/devices-mine.ts +103 -0
  45. package/src/http/routes/feature-flags.ts +1 -8
  46. package/src/http/routes/guardian-pin.ts +2 -11
  47. package/src/http/routes/ipc-runtime-proxy.ts +1 -1
  48. package/src/http/routes/live-voice-websocket.ts +1 -1
  49. package/src/http/routes/runtime-audio-stream.ts +1 -1
  50. package/src/http/routes/runtime-proxy.ts +2 -2
  51. package/src/http/routes/twilio-voice-webhook.test.ts +13 -13
  52. package/src/index.ts +11 -198
  53. package/src/ipc/feature-flag-handlers.ts +2 -9
  54. package/src/remote-web/pairing-challenge-store.ts +15 -0
  55. package/src/risk/command-registry/commands/assistant.ts +0 -24
  56. package/src/risk/command-registry.test.ts +2 -4
  57. package/src/schema.ts +44 -372
  58. package/src/__tests__/auth-fallback-count-tracker.test.ts +0 -92
  59. package/src/__tests__/auth-fallback-reporter.test.ts +0 -101
  60. package/src/__tests__/backup-worker-cloud-mode.test.ts +0 -44
  61. package/src/__tests__/backup-worker-export-timeout.test.ts +0 -164
  62. package/src/__tests__/migration-teleport-gcs-proxy.test.ts +0 -216
  63. package/src/__tests__/remote-feature-flag-sync.test.ts +0 -961
  64. package/src/auth-fallback-count-tracker.ts +0 -113
  65. package/src/auth-fallback-reporter.ts +0 -127
  66. package/src/backup/backup-key.ts +0 -138
  67. package/src/backup/backup-routes.ts +0 -161
  68. package/src/backup/backup-worker.ts +0 -425
  69. package/src/backup/list-snapshots.ts +0 -97
  70. package/src/backup/local-writer.ts +0 -87
  71. package/src/backup/offsite-writer.ts +0 -182
  72. package/src/backup/paths.ts +0 -113
  73. package/src/backup/stream-crypt.ts +0 -258
  74. package/src/device-id.ts +0 -18
  75. package/src/feature-flag-remote-store.ts +0 -166
  76. package/src/http/routes/migration-proxy.ts +0 -636
  77. package/src/internal-telemetry-client.ts +0 -39
  78. package/src/ipc/debug-export-handlers.test.ts +0 -102
  79. package/src/ipc/debug-export-handlers.ts +0 -81
  80. package/src/remote-feature-flag-sync.ts +0 -440
  81. package/src/watchdog-reporter.ts +0 -110
package/src/schema.ts CHANGED
@@ -3989,309 +3989,6 @@ export function buildSchema(): Record<string, unknown> {
3989
3989
  },
3990
3990
  },
3991
3991
  },
3992
- "/v1/migrations/export": {
3993
- post: {
3994
- summary: "Export workspace backup",
3995
- description:
3996
- "Proxies a migration export request to the assistant daemon. Returns a binary .vbundle backup of the workspace. Authenticated with an edge JWT. Timeout is 120 seconds to accommodate large workspaces.",
3997
- operationId: "migrationExport",
3998
- security: [{ BearerAuth: [] }],
3999
- requestBody: {
4000
- required: false,
4001
- content: {
4002
- "application/json": {
4003
- schema: { type: "object", additionalProperties: true },
4004
- },
4005
- },
4006
- },
4007
- responses: {
4008
- "200": {
4009
- description: "Binary .vbundle backup file",
4010
- content: {
4011
- "application/octet-stream": {
4012
- schema: { type: "string", format: "binary" },
4013
- },
4014
- },
4015
- },
4016
- "401": {
4017
- description: "Unauthorized — missing or invalid bearer token",
4018
- },
4019
- "502": { description: "Failed to reach assistant daemon" },
4020
- "504": { description: "Assistant daemon request timed out" },
4021
- },
4022
- },
4023
- },
4024
- "/v1/migrations/import": {
4025
- post: {
4026
- summary: "Import workspace backup",
4027
- description:
4028
- "Proxies a migration import request to the assistant. Request shapes:\n" +
4029
- "\n" +
4030
- " - `application/octet-stream`: raw .vbundle body. The request is proxied synchronously and the caller's connection stays open for the full import duration (returns 200 on success).\n" +
4031
- ' - `application/json` with `{ "url": "<signed GCS URL>" }` or `{ "path": "<staged workspace path>" }`: the gateway generates a jobId, kicks off the upstream assistant call in the background, and returns `202 Accepted` with `{ job_id, status: "pending" }` immediately. Callers poll `GET /v1/migrations/import/{jobId}/status` for progress. The path form points at a `.vbundle` staged under the assistant workspace `.restore-staging` directory.\n' +
4032
- "\n" +
4033
- "Authenticated with an edge JWT. Synchronous-path timeout is 60 minutes to accommodate large 8 GB backups; the async path returns immediately.",
4034
- operationId: "migrationImport",
4035
- security: [{ BearerAuth: [] }],
4036
- requestBody: {
4037
- required: true,
4038
- content: {
4039
- "application/octet-stream": {
4040
- schema: { type: "string", format: "binary" },
4041
- },
4042
- "application/json": {
4043
- schema: {
4044
- oneOf: [
4045
- {
4046
- type: "object",
4047
- required: ["url"],
4048
- properties: {
4049
- url: {
4050
- type: "string",
4051
- format: "uri",
4052
- description:
4053
- "Signed GCS URL pointing at a .vbundle archive.",
4054
- },
4055
- },
4056
- },
4057
- {
4058
- type: "object",
4059
- required: ["path"],
4060
- properties: {
4061
- path: {
4062
- type: "string",
4063
- minLength: 1,
4064
- description:
4065
- "Workspace-relative or absolute path to a staged .vbundle under .restore-staging.",
4066
- },
4067
- },
4068
- },
4069
- ],
4070
- },
4071
- },
4072
- },
4073
- },
4074
- responses: {
4075
- "200": {
4076
- description:
4077
- "Backup imported successfully (synchronous byte path).",
4078
- },
4079
- "202": {
4080
- description:
4081
- "Import accepted for async processing (JSON url or path body). Poll `/v1/migrations/import/{jobId}/status` for progress.",
4082
- content: {
4083
- "application/json": {
4084
- schema: {
4085
- type: "object",
4086
- required: ["job_id", "status"],
4087
- properties: {
4088
- job_id: { type: "string" },
4089
- status: { type: "string", enum: ["pending"] },
4090
- },
4091
- },
4092
- },
4093
- },
4094
- },
4095
- "401": {
4096
- description: "Unauthorized — missing or invalid bearer token",
4097
- },
4098
- "502": { description: "Failed to reach assistant daemon" },
4099
- "504": { description: "Assistant daemon request timed out" },
4100
- },
4101
- },
4102
- },
4103
- "/v1/migrations/import/{jobId}/status": {
4104
- get: {
4105
- summary: "Poll async import job status",
4106
- description:
4107
- "Returns the current status of an async `.vbundle` import kicked off by `POST /v1/migrations/import` with a JSON `{url}` or `{path}` body. Finished jobs remain queryable for 30 minutes before being pruned.",
4108
- operationId: "migrationImportStatus",
4109
- security: [{ BearerAuth: [] }],
4110
- parameters: [
4111
- {
4112
- in: "path",
4113
- name: "jobId",
4114
- required: true,
4115
- schema: { type: "string" },
4116
- description:
4117
- "The `job_id` returned by the 202 response from `POST /v1/migrations/import`.",
4118
- },
4119
- ],
4120
- responses: {
4121
- "200": {
4122
- description: "Current job status.",
4123
- content: {
4124
- "application/json": {
4125
- schema: {
4126
- type: "object",
4127
- required: ["job_id", "status"],
4128
- properties: {
4129
- job_id: { type: "string" },
4130
- status: {
4131
- type: "string",
4132
- enum: ["pending", "processing", "complete", "failed"],
4133
- },
4134
- error: { type: "string" },
4135
- result: {},
4136
- },
4137
- },
4138
- },
4139
- },
4140
- },
4141
- "401": {
4142
- description: "Unauthorized — missing or invalid bearer token",
4143
- },
4144
- "404": {
4145
- description:
4146
- "Unknown job — never issued, or pruned after 30-minute TTL.",
4147
- },
4148
- },
4149
- },
4150
- },
4151
- "/v1/migrations/export-to-gcs": {
4152
- post: {
4153
- summary: "Kick off an async export-to-GCS job",
4154
- description:
4155
- "Transparent proxy to the assistant daemon's `POST /v1/migrations/export-to-gcs` endpoint. The daemon schedules a background export job that streams the workspace backup to the supplied signed GCS upload URL and returns `202 Accepted` with a `job_id`. Callers poll `GET /v1/migrations/jobs/{jobId}` for progress. Registered explicitly, not via the runtime-proxy catch-all, for dedicated auth and timeout handling.",
4156
- operationId: "migrationExportToGcs",
4157
- security: [{ BearerAuth: [] }],
4158
- requestBody: {
4159
- required: true,
4160
- content: {
4161
- "application/json": {
4162
- schema: { type: "object", additionalProperties: true },
4163
- },
4164
- },
4165
- },
4166
- responses: {
4167
- "202": {
4168
- description: "Export job accepted.",
4169
- content: {
4170
- "application/json": {
4171
- schema: {
4172
- type: "object",
4173
- required: ["job_id"],
4174
- properties: { job_id: { type: "string" } },
4175
- },
4176
- },
4177
- },
4178
- },
4179
- "401": {
4180
- description: "Unauthorized — missing or invalid bearer token",
4181
- },
4182
- "409": {
4183
- description: "Another export is already in flight.",
4184
- },
4185
- "502": { description: "Failed to reach assistant daemon" },
4186
- "504": { description: "Assistant daemon request timed out" },
4187
- },
4188
- },
4189
- },
4190
- "/v1/migrations/import-from-gcs": {
4191
- post: {
4192
- summary: "Kick off an async import-from-GCS job",
4193
- description:
4194
- "Transparent proxy to the assistant daemon's `POST /v1/migrations/import-from-gcs` endpoint. The daemon schedules a background import job that fetches a `.vbundle` archive at the supplied `bundle_url` and streams it through the importer, returning `202 Accepted` with a `job_id`. Callers poll `GET /v1/migrations/jobs/{jobId}` for progress. Registered explicitly, not via the runtime-proxy catch-all, for dedicated auth and timeout handling.",
4195
- operationId: "migrationImportFromGcs",
4196
- security: [{ BearerAuth: [] }],
4197
- requestBody: {
4198
- required: true,
4199
- content: {
4200
- "application/json": {
4201
- schema: {
4202
- type: "object",
4203
- required: ["bundle_url"],
4204
- properties: {
4205
- bundle_url: {
4206
- type: "string",
4207
- format: "uri",
4208
- description:
4209
- "Signed GCS URL pointing at a .vbundle archive.",
4210
- },
4211
- },
4212
- },
4213
- },
4214
- },
4215
- },
4216
- responses: {
4217
- "202": {
4218
- description: "Import job accepted.",
4219
- content: {
4220
- "application/json": {
4221
- schema: {
4222
- type: "object",
4223
- required: ["job_id"],
4224
- properties: { job_id: { type: "string" } },
4225
- },
4226
- },
4227
- },
4228
- },
4229
- "401": {
4230
- description: "Unauthorized — missing or invalid bearer token",
4231
- },
4232
- "409": {
4233
- description: "Another import is already in flight.",
4234
- },
4235
- "502": { description: "Failed to reach assistant daemon" },
4236
- "504": { description: "Assistant daemon request timed out" },
4237
- },
4238
- },
4239
- },
4240
- "/v1/migrations/jobs/{jobId}": {
4241
- get: {
4242
- summary: "Poll unified migration job status",
4243
- description:
4244
- "Transparent proxy to the assistant daemon's `GET /v1/migrations/jobs/{job_id}` endpoint. Returns the current status of an async export-to-GCS or import-from-GCS job tracked by the daemon's migration job registry. Registered explicitly, not via the runtime-proxy catch-all, for dedicated auth and timeout handling.",
4245
- operationId: "migrationJobStatus",
4246
- security: [{ BearerAuth: [] }],
4247
- parameters: [
4248
- {
4249
- in: "path",
4250
- name: "jobId",
4251
- required: true,
4252
- schema: { type: "string" },
4253
- description:
4254
- "The `job_id` returned by the 202 response from `POST /v1/migrations/export-to-gcs` or `POST /v1/migrations/import-from-gcs`.",
4255
- },
4256
- ],
4257
- responses: {
4258
- "200": {
4259
- description: "Current job status.",
4260
- content: {
4261
- "application/json": {
4262
- schema: {
4263
- type: "object",
4264
- required: ["job_id", "type", "status"],
4265
- properties: {
4266
- job_id: { type: "string" },
4267
- type: {
4268
- type: "string",
4269
- enum: ["export", "import"],
4270
- },
4271
- status: {
4272
- type: "string",
4273
- enum: ["processing", "complete", "failed"],
4274
- },
4275
- result: {},
4276
- error: { type: "string" },
4277
- error_code: { type: "string" },
4278
- upstream_status: { type: "integer" },
4279
- },
4280
- },
4281
- },
4282
- },
4283
- },
4284
- "401": {
4285
- description: "Unauthorized — missing or invalid bearer token",
4286
- },
4287
- "404": {
4288
- description: "Unknown job_id.",
4289
- },
4290
- "502": { description: "Failed to reach assistant daemon" },
4291
- "504": { description: "Assistant daemon request timed out" },
4292
- },
4293
- },
4294
- },
4295
3992
  "/v1/admin/workspace-commit": {
4296
3993
  post: {
4297
3994
  summary: "Commit workspace changes",
@@ -4431,50 +4128,37 @@ export function buildSchema(): Record<string, unknown> {
4431
4128
  },
4432
4129
  },
4433
4130
  },
4434
- "/v1/backups": {
4131
+ "/v1/devices/mine": {
4435
4132
  get: {
4436
- summary: "List backup snapshots",
4133
+ summary: "The caller's own paired-device row",
4437
4134
  description:
4438
- "Lists local and offsite backup snapshots. The gateway owns the backup encryption key and performs all encrypt/decrypt operations.",
4439
- operationId: "backupsList",
4135
+ "Returns the paired-device row matching the presenting bearer token, so the paired web UI can surface 'this device' for matching against the host-side `cadet devices` list. Browser-reachable: the bearer is the credential, and the route answers only for the token presenting it. Not available on cloud-hosted assistants (they have no paired-device rows).",
4136
+ operationId: "getMyDevice",
4440
4137
  security: [{ BearerAuth: [] }],
4441
4138
  responses: {
4442
4139
  "200": {
4443
- description: "Backup snapshots listed",
4140
+ description: "The caller's paired-device row",
4444
4141
  content: {
4445
4142
  "application/json": {
4446
4143
  schema: {
4447
4144
  type: "object",
4448
- required: ["local", "offsite"],
4145
+ required: ["device"],
4449
4146
  properties: {
4450
- local: {
4147
+ device: {
4451
4148
  type: "object",
4452
- required: ["directory", "snapshots"],
4149
+ required: ["hashedDeviceId", "platform", "issuedAt"],
4453
4150
  properties: {
4454
- directory: { type: "string" },
4455
- snapshots: {
4456
- type: "array",
4457
- items: {
4458
- $ref: "#/components/schemas/BackupSnapshot",
4459
- },
4151
+ hashedDeviceId: { type: "string" },
4152
+ platform: { type: "string" },
4153
+ pairingUserAgent: {
4154
+ type: ["string", "null"],
4460
4155
  },
4461
- },
4462
- },
4463
- offsite: {
4464
- type: "array",
4465
- items: {
4466
- type: "object",
4467
- required: ["directory", "encrypted", "snapshots"],
4468
- properties: {
4469
- directory: { type: "string" },
4470
- encrypted: { type: "boolean" },
4471
- snapshots: {
4472
- type: "array",
4473
- items: {
4474
- $ref: "#/components/schemas/BackupSnapshot",
4475
- },
4476
- },
4156
+ clientReportedName: {
4157
+ type: ["string", "null"],
4477
4158
  },
4159
+ issuedAt: { type: "string" },
4160
+ expiresAt: { type: ["string", "null"] },
4161
+ lastUsedAt: { type: ["string", "null"] },
4478
4162
  },
4479
4163
  },
4480
4164
  },
@@ -4483,51 +4167,22 @@ export function buildSchema(): Record<string, unknown> {
4483
4167
  },
4484
4168
  },
4485
4169
  "401": {
4486
- description: "Unauthorized — missing or invalid bearer token",
4487
- },
4488
- "403": { description: "Insufficient scope" },
4489
- "500": { description: "Internal server error" },
4490
- },
4491
- },
4492
- },
4493
- "/v1/backups/create": {
4494
- post: {
4495
- summary: "Create backup snapshot",
4496
- description:
4497
- "Triggers a manual backup snapshot. The gateway exports a plaintext vbundle from the daemon, writes it locally, and encrypts + mirrors to offsite destinations.",
4498
- operationId: "backupsCreate",
4499
- security: [{ BearerAuth: [] }],
4500
- responses: {
4501
- "200": {
4502
- description: "Backup snapshot created",
4170
+ description: "Missing or invalid bearer token",
4503
4171
  content: {
4504
4172
  "application/json": {
4505
- schema: {
4506
- type: "object",
4507
- required: ["success", "local", "offsite", "duration_ms"],
4508
- properties: {
4509
- success: { type: "boolean" },
4510
- local: {
4511
- $ref: "#/components/schemas/BackupSnapshot",
4512
- },
4513
- offsite: {
4514
- type: "array",
4515
- items: { type: "object", additionalProperties: true },
4516
- },
4517
- duration_ms: { type: "number" },
4518
- },
4519
- },
4173
+ schema: { $ref: "#/components/schemas/ErrorResponse" },
4520
4174
  },
4521
4175
  },
4522
4176
  },
4523
- "401": {
4524
- description: "Unauthorized — missing or invalid bearer token",
4525
- },
4526
- "403": { description: "Insufficient scope" },
4527
- "409": {
4528
- description: "A backup snapshot is already in progress",
4177
+ "404": {
4178
+ description:
4179
+ "No paired-device row for this token, or the gateway is cloud-hosted",
4180
+ content: {
4181
+ "application/json": {
4182
+ schema: { $ref: "#/components/schemas/ErrorResponse" },
4183
+ },
4184
+ },
4529
4185
  },
4530
- "500": { description: "Internal server error" },
4531
4186
  },
4532
4187
  },
4533
4188
  },
@@ -4628,6 +4283,23 @@ export function buildSchema(): Record<string, unknown> {
4628
4283
  },
4629
4284
  },
4630
4285
  },
4286
+ "404": {
4287
+ description: "Route not found on upstream runtime",
4288
+ content: {
4289
+ "application/json": {
4290
+ schema: { $ref: "#/components/schemas/ErrorResponse" },
4291
+ },
4292
+ },
4293
+ },
4294
+ "500": {
4295
+ description:
4296
+ "Server misconfigured (proxy auth enabled without token)",
4297
+ content: {
4298
+ "application/json": {
4299
+ schema: { $ref: "#/components/schemas/ErrorResponse" },
4300
+ },
4301
+ },
4302
+ },
4631
4303
  "502": {
4632
4304
  description: "Upstream connection failed",
4633
4305
  content: {
@@ -1,92 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
-
3
- import { AuthFallbackCountTracker } from "../auth-fallback-count-tracker.js";
4
-
5
- describe("AuthFallbackCountTracker", () => {
6
- test("increments per (guard, path, failureKind) key", () => {
7
- const t = new AuthFallbackCountTracker(0);
8
- t.increment("edge", "/v1/chat", "missing_authorization");
9
- t.increment("edge", "/v1/chat", "missing_authorization");
10
- t.increment("edge", "/v1/chat", "token_validation_failed");
11
- t.increment("edge-guardian", "/v1/chat", "missing_authorization");
12
-
13
- const snap = t.snapshot();
14
- expect(snap.length).toBe(3);
15
- const find = (g: string, p: string, f: string) =>
16
- snap.find((c) => c.guard === g && c.path === p && c.failureKind === f)
17
- ?.count;
18
- expect(find("edge", "/v1/chat", "missing_authorization")).toBe(2);
19
- expect(find("edge", "/v1/chat", "token_validation_failed")).toBe(1);
20
- expect(find("edge-guardian", "/v1/chat", "missing_authorization")).toBe(1);
21
- });
22
-
23
- test("drain returns the window + counts and resets", () => {
24
- const t = new AuthFallbackCountTracker(1000);
25
- t.increment("edge", "/v1/a", "missing_authorization");
26
- t.increment("edge", "/v1/a", "missing_authorization");
27
-
28
- const batch = t.drain(2000);
29
- expect(batch.windowStart).toBe(1000);
30
- expect(batch.windowEnd).toBe(2000);
31
- expect(batch.counts).toEqual([
32
- {
33
- guard: "edge",
34
- path: "/v1/a",
35
- failureKind: "missing_authorization",
36
- count: 2,
37
- },
38
- ]);
39
-
40
- // Drained — tracker is empty and the window is re-anchored to the drain.
41
- expect(t.snapshot()).toEqual([]);
42
- const empty = t.drain(3000);
43
- expect(empty.counts).toEqual([]);
44
- expect(empty.windowStart).toBe(2000);
45
- expect(empty.windowEnd).toBe(3000);
46
- });
47
-
48
- test("an empty drain leaves the window start anchored", () => {
49
- const t = new AuthFallbackCountTracker(1000);
50
- // Nothing recorded yet — draining must not shift windowStart forward.
51
- expect(t.drain(2000)).toMatchObject({ windowStart: 1000, windowEnd: 2000 });
52
- t.increment("edge", "/v1/a", "missing_authorization");
53
- expect(t.drain(3000).windowStart).toBe(1000);
54
- });
55
-
56
- test("merge folds a drained batch back in", () => {
57
- const t = new AuthFallbackCountTracker(0);
58
- t.increment("edge", "/v1/a", "missing_authorization");
59
- const batch = t.drain(100);
60
-
61
- // Simulate a failed flush: counts come back, plus a newer count for the
62
- // same key that accumulated after the drain.
63
- t.increment("edge", "/v1/a", "missing_authorization");
64
- t.merge(batch.counts);
65
-
66
- const snap = t.snapshot();
67
- expect(snap.length).toBe(1);
68
- expect(snap[0].count).toBe(2);
69
- });
70
-
71
- test("caps distinct keys but keeps counting existing ones", () => {
72
- const t = new AuthFallbackCountTracker(0);
73
- // MAX_TRACKED_KEYS is 10_000; exceed it with distinct paths.
74
- for (let i = 0; i < 10_050; i++) {
75
- t.increment("edge", `/v1/p${i}`, "missing_authorization");
76
- }
77
- const snap = t.snapshot();
78
- expect(snap.length).toBe(10_000);
79
-
80
- // An already-tracked key still increments past the cap.
81
- t.increment("edge", "/v1/p0", "missing_authorization");
82
- expect(t.snapshot().find((c) => c.path === "/v1/p0")?.count).toBe(2);
83
- });
84
-
85
- test("reset clears counts", () => {
86
- const t = new AuthFallbackCountTracker(0);
87
- t.increment("edge", "/v1/a", "missing_authorization");
88
- t.reset(500);
89
- expect(t.snapshot()).toEqual([]);
90
- expect(t.drain(600).windowStart).toBe(500);
91
- });
92
- });
@@ -1,101 +0,0 @@
1
- import { describe, expect, mock, test } from "bun:test";
2
-
3
- import { AuthFallbackCountTracker } from "../auth-fallback-count-tracker.js";
4
- import { AuthFallbackReporter } from "../auth-fallback-reporter.js";
5
-
6
- const BASE_URL = "http://127.0.0.1:7821";
7
-
8
- function makeReporter(
9
- tracker: AuthFallbackCountTracker,
10
- fetchImpl: typeof import("../fetch.js").fetchImpl,
11
- ) {
12
- return new AuthFallbackReporter({
13
- tracker,
14
- baseUrl: BASE_URL,
15
- intervalMs: 60_000,
16
- fetchImpl,
17
- mintToken: () => "test-service-token",
18
- });
19
- }
20
-
21
- describe("AuthFallbackReporter", () => {
22
- test("does nothing when there is nothing to flush", async () => {
23
- const tracker = new AuthFallbackCountTracker(0);
24
- const fetchMock = mock(async () => new Response("{}", { status: 200 }));
25
- await makeReporter(tracker, fetchMock as never).flush();
26
- expect(fetchMock).not.toHaveBeenCalled();
27
- });
28
-
29
- test("drains and POSTs the counts to the daemon route with a service token", async () => {
30
- const tracker = new AuthFallbackCountTracker(0);
31
- tracker.increment("edge", "/v1/chat", "missing_authorization");
32
- tracker.increment("edge", "/v1/chat", "missing_authorization");
33
- tracker.increment("edge-guardian", "/v1/sync", "guardian_mismatch");
34
-
35
- const fetchMock = mock(async () => new Response("{}", { status: 200 }));
36
- await makeReporter(tracker, fetchMock as never).flush();
37
-
38
- expect(fetchMock).toHaveBeenCalledTimes(1);
39
- const [url, init] = fetchMock.mock.calls[0] as unknown as [
40
- string,
41
- RequestInit,
42
- ];
43
- expect(url).toBe(`${BASE_URL}/v1/internal/telemetry/auth-fallback`);
44
- expect(init.method).toBe("POST");
45
- const headers = init.headers as Record<string, string>;
46
- expect(headers["Content-Type"]).toBe("application/json");
47
- expect(headers["Authorization"]).toBe("Bearer test-service-token");
48
-
49
- const body = JSON.parse(init.body as string);
50
- expect(typeof body.window_start).toBe("number");
51
- expect(typeof body.window_end).toBe("number");
52
- expect(body.counts).toEqual(
53
- expect.arrayContaining([
54
- {
55
- guard: "edge",
56
- path: "/v1/chat",
57
- failure_kind: "missing_authorization",
58
- count: 2,
59
- },
60
- {
61
- guard: "edge-guardian",
62
- path: "/v1/sync",
63
- failure_kind: "guardian_mismatch",
64
- count: 1,
65
- },
66
- ]),
67
- );
68
-
69
- // Successful flush drains the tracker.
70
- expect(tracker.snapshot()).toEqual([]);
71
- });
72
-
73
- test("re-queues counts when the daemon returns a non-OK status", async () => {
74
- const tracker = new AuthFallbackCountTracker(0);
75
- tracker.increment("edge", "/v1/chat", "missing_authorization");
76
-
77
- const fetchMock = mock(async () => new Response("nope", { status: 404 }));
78
- await makeReporter(tracker, fetchMock as never).flush();
79
-
80
- expect(fetchMock).toHaveBeenCalledTimes(1);
81
- // Counts merged back so the next flush retries them.
82
- const snap = tracker.snapshot();
83
- expect(snap.length).toBe(1);
84
- expect(snap[0].count).toBe(1);
85
- });
86
-
87
- test("re-queues counts when the POST throws", async () => {
88
- const tracker = new AuthFallbackCountTracker(0);
89
- tracker.increment("edge", "/v1/chat", "missing_authorization");
90
- tracker.increment("edge", "/v1/chat", "missing_authorization");
91
-
92
- const fetchMock = mock(async () => {
93
- throw new Error("connection refused");
94
- });
95
- await makeReporter(tracker, fetchMock as never).flush();
96
-
97
- const snap = tracker.snapshot();
98
- expect(snap.length).toBe(1);
99
- expect(snap[0].count).toBe(2);
100
- });
101
- });