@camstack/types 1.2.54 → 1.2.56

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.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_event_category = require("./event-category-BE4PDZ_3.js");
3
- const require_sleep = require("./sleep-DfF6vKCf.js");
2
+ const require_event_category = require("./event-category-DxZbWydC.js");
3
+ const require_sleep = require("./sleep-EX1-qUVR.js");
4
4
  const require_canonical_hash = require("./canonical-hash-DNV8S5ET.js");
5
5
  const require_enums = require("./enums.js");
6
6
  const require_err_msg = require("./err-msg-COpsHMw2.js");
@@ -2469,6 +2469,102 @@ var ConvertResultSchema = zod.z.object({
2469
2469
  artifacts: zod.z.array(ConvertArtifactSchema).readonly()
2470
2470
  });
2471
2471
  //#endregion
2472
+ //#region src/auth/principal-scope.ts
2473
+ /**
2474
+ * The access flavour an HTTP verb asks for. `GET`/`HEAD` read; anything that
2475
+ * can change state is at least `create`.
2476
+ *
2477
+ * An UNRECOGNISED verb resolves to `delete`, the most privileged flavour, so a
2478
+ * method nobody thought about fails closed. The inverse default — treating the
2479
+ * unknown as `view` — is how a gate quietly stops gating.
2480
+ */
2481
+ function methodAccessForHttpMethod(method) {
2482
+ switch (method.toUpperCase()) {
2483
+ case "GET":
2484
+ case "HEAD":
2485
+ case "OPTIONS": return "view";
2486
+ case "POST":
2487
+ case "PUT":
2488
+ case "PATCH": return "create";
2489
+ default: return "delete";
2490
+ }
2491
+ }
2492
+ /**
2493
+ * True if the scope set grants `access` on device-scoped capabilities.
2494
+ * A `category:device` grant covers every device cap (the broad grant).
2495
+ */
2496
+ function scopesAllowDeviceCap(scopes, access) {
2497
+ return scopes.some((s) => s.type === "category" && s.target === "device" && s.access.includes(access));
2498
+ }
2499
+ /**
2500
+ * True if the scope set names this addon with at least `access`.
2501
+ *
2502
+ * Deliberately NOT satisfied by `category:device`: a grant over per-camera
2503
+ * capabilities says nothing about an addon's own HTTP surface, and conflating
2504
+ * the two is precisely the confusion that let an Alexa token drive Home
2505
+ * Assistant's command route.
2506
+ */
2507
+ function scopesAllowAddon(scopes, addonId, access) {
2508
+ return scopes.some((s) => s.type === "addon" && s.target === addonId && s.access.includes(access));
2509
+ }
2510
+ /**
2511
+ * Classify a VERIFIED JWT payload.
2512
+ *
2513
+ * A session JWT carries no `kind`. Everything else in this system tags itself:
2514
+ * `kind: 'sso-bridge'` for the OAuth/SSO family (discriminated further by
2515
+ * `provider`), `kind: 'totp-challenge'` for login leg 1. Only `oauth-access` is
2516
+ * an API credential; a code lives in a browser redirect URL and a refresh token
2517
+ * belongs to `/token`.
2518
+ */
2519
+ function classifyBearerPrincipal(payload) {
2520
+ const kind = payload.kind;
2521
+ if (kind === void 0) return { kind: "session" };
2522
+ if (kind !== "sso-bridge") return {
2523
+ kind: "not-a-credential",
2524
+ reason: `bridge token kind=${String(kind)}`
2525
+ };
2526
+ const provider = payload.provider;
2527
+ if (provider === "oauth-access") return {
2528
+ kind: "integration",
2529
+ provider: "oauth-access"
2530
+ };
2531
+ return {
2532
+ kind: "not-a-credential",
2533
+ reason: `bridge token provider=${String(provider)}`
2534
+ };
2535
+ }
2536
+ /**
2537
+ * May this principal reach `/addon/<addonId>/…` on an `authenticated` route or
2538
+ * data-plane endpoint?
2539
+ *
2540
+ * Three principals, three answers:
2541
+ *
2542
+ * - **session** — unchanged. A user session is governed by the route's own
2543
+ * `access` (`authenticated` / `admin`), exactly as before. Tightening HERE
2544
+ * would lock every non-admin viewer out of recorder playback, snapshot media
2545
+ * and the stream-broker embed, which is a worse outcome than the hole.
2546
+ * - **integration** — must hold `addon:<addonId>` at the verb's access. This is
2547
+ * the same rule the `cst_` scoped-token branch has always applied; the JWT
2548
+ * branch simply never applied it, and closing that divergence IS the fix.
2549
+ * `isAdmin` is ignored: an integration token is minted `isAdmin: false` and a
2550
+ * token claiming otherwise must not talk its way past the grant.
2551
+ * - **not-a-credential** — refused unconditionally.
2552
+ */
2553
+ function principalMayReachAddon(input) {
2554
+ const { principal } = input;
2555
+ if (principal.kind === "not-a-credential") return {
2556
+ allowed: false,
2557
+ reason: principal.reason
2558
+ };
2559
+ if (principal.kind === "session") return { allowed: true };
2560
+ const access = methodAccessForHttpMethod(input.method);
2561
+ if (scopesAllowAddon(input.scopes, input.addonId, access)) return { allowed: true };
2562
+ return {
2563
+ allowed: false,
2564
+ reason: `integration token holds no addon:${input.addonId}[${access}] grant`
2565
+ };
2566
+ }
2567
+ //#endregion
2472
2568
  //#region src/expression/errors.ts
2473
2569
  /**
2474
2570
  * Error types for the safe expression engine. Two distinct classes so callers
@@ -11563,9 +11659,36 @@ var NcDeliverySchema = zod.z.enum([
11563
11659
  "immediate",
11564
11660
  "track-end",
11565
11661
  "device-event",
11566
- "package-event"
11662
+ "package-event",
11663
+ "system-event"
11664
+ ]);
11665
+ /**
11666
+ * Stable Notification Center vocabulary over infrastructure/liveness events.
11667
+ * Bus categories are normalized into these intent-level kinds so rules do not
11668
+ * depend on a provider's raw event name or payload shape.
11669
+ */
11670
+ var NcSystemEventKindSchema = zod.z.enum([
11671
+ "camera-online",
11672
+ "camera-offline",
11673
+ "stream-online",
11674
+ "stream-offline",
11675
+ "node-online",
11676
+ "node-offline",
11677
+ "addon-update-available",
11678
+ "server-update-available"
11567
11679
  ]);
11568
11680
  /**
11681
+ * One coherent system-event condition. `kinds` is the required opt-in safety
11682
+ * gate; the remaining lists are optional narrowing filters relevant to the
11683
+ * selected kinds.
11684
+ */
11685
+ var NcSystemEventConditionSchema = zod.z.object({
11686
+ kinds: zod.z.array(NcSystemEventKindSchema).min(1),
11687
+ deviceIds: zod.z.array(zod.z.number().int()).min(1).optional(),
11688
+ nodeIds: zod.z.array(zod.z.string().min(1)).min(1).optional(),
11689
+ packageNames: zod.z.array(zod.z.string().min(1)).min(1).optional()
11690
+ });
11691
+ /**
11569
11692
  * `maxPerTrack` for `immediate` rules is FIXED at 1 (D-3): a single track
11570
11693
  * fires an immediate rule at most once, enforced durably by the outbox
11571
11694
  * unique key `(ruleId, trackId, targetId)`. Not a rule field in P1.
@@ -11888,6 +12011,8 @@ var NcConditionsSchema = zod.z.object({
11888
12011
  "picked-up",
11889
12012
  "both"
11890
12013
  ]).optional(),
12014
+ /** Infrastructure/liveness/update event matcher (`system-event` delivery). */
12015
+ systemEvent: NcSystemEventConditionSchema.optional(),
11891
12016
  /**
11892
12017
  * PERSONAL-RULE custom zones (viewer-drawn). Inline normalized polygons
11893
12018
  * (MaskShape vocabulary). A record passes when its bbox overlaps ANY
@@ -12107,7 +12232,8 @@ var NcTestResultSchema = zod.z.object({
12107
12232
  "object-event",
12108
12233
  "track",
12109
12234
  "device-event",
12110
- "package-event"
12235
+ "package-event",
12236
+ "system-event"
12111
12237
  ]),
12112
12238
  deviceId: zod.z.number(),
12113
12239
  timestamp: zod.z.number(),
@@ -12129,7 +12255,8 @@ var NcConditionDescriptorSchema = zod.z.object({
12129
12255
  "schedule",
12130
12256
  "device",
12131
12257
  "package",
12132
- "occupancy"
12258
+ "occupancy",
12259
+ "system"
12133
12260
  ]),
12134
12261
  label: zod.z.string(),
12135
12262
  /** Editor widget the UI renders — never hardcode per-condition forms. */
@@ -12147,7 +12274,8 @@ var NcConditionDescriptorSchema = zod.z.object({
12147
12274
  "crossingSelect",
12148
12275
  "polygonDraw",
12149
12276
  "occupancy",
12150
- "deviceState"
12277
+ "deviceState",
12278
+ "systemEvent"
12151
12279
  ]),
12152
12280
  operator: zod.z.enum([
12153
12281
  "in",
@@ -12189,6 +12317,50 @@ var NcConditionDescriptorSchema = zod.z.object({
12189
12317
  * rule editors render from the catalog, not hardcoded forms (spec §4.2).
12190
12318
  */
12191
12319
  var NC_CONDITION_CATALOG = [
12320
+ {
12321
+ id: "systemEvent",
12322
+ group: "system",
12323
+ label: "System event",
12324
+ valueType: "systemEvent",
12325
+ options: [
12326
+ {
12327
+ value: "camera-online",
12328
+ label: "Camera online"
12329
+ },
12330
+ {
12331
+ value: "camera-offline",
12332
+ label: "Camera offline"
12333
+ },
12334
+ {
12335
+ value: "stream-online",
12336
+ label: "Stream online"
12337
+ },
12338
+ {
12339
+ value: "stream-offline",
12340
+ label: "Stream offline"
12341
+ },
12342
+ {
12343
+ value: "node-online",
12344
+ label: "Node online"
12345
+ },
12346
+ {
12347
+ value: "node-offline",
12348
+ label: "Node offline"
12349
+ },
12350
+ {
12351
+ value: "addon-update-available",
12352
+ label: "Addon update available"
12353
+ },
12354
+ {
12355
+ value: "server-update-available",
12356
+ label: "Server update available"
12357
+ }
12358
+ ],
12359
+ operator: "in",
12360
+ appliesTo: ["system-event"],
12361
+ phase: "P1",
12362
+ description: "Infrastructure and update events. Optionally narrow camera/stream events by device, node events by node id, and addon updates by package name."
12363
+ },
12192
12364
  {
12193
12365
  id: "devices",
12194
12366
  group: "scope",
@@ -12503,7 +12675,8 @@ var NC_CONDITION_CATALOG = [
12503
12675
  "immediate",
12504
12676
  "track-end",
12505
12677
  "device-event",
12506
- "package-event"
12678
+ "package-event",
12679
+ "system-event"
12507
12680
  ],
12508
12681
  phase: "P1",
12509
12682
  description: "Weekly activation windows (invertible); absent = always active."
@@ -12531,7 +12704,8 @@ var NcHistoryRecordKindSchema = zod.z.enum([
12531
12704
  "object-event",
12532
12705
  "track-end",
12533
12706
  "device-event",
12534
- "package-event"
12707
+ "package-event",
12708
+ "system-event"
12535
12709
  ]);
12536
12710
  /** Subject summary frozen on the row at fire time (survives rule/record edits). */
12537
12711
  var NcHistorySubjectSchema = zod.z.object({
@@ -12539,7 +12713,14 @@ var NcHistorySubjectSchema = zod.z.object({
12539
12713
  label: zod.z.string().optional(),
12540
12714
  confidence: zod.z.number().optional(),
12541
12715
  zones: zod.z.array(zod.z.string()),
12542
- timestamp: zod.z.number()
12716
+ timestamp: zod.z.number(),
12717
+ systemEvent: zod.z.object({
12718
+ kind: NcSystemEventKindSchema,
12719
+ subject: zod.z.string(),
12720
+ title: zod.z.string(),
12721
+ body: zod.z.string(),
12722
+ data: zod.z.record(zod.z.string(), zod.z.unknown())
12723
+ }).optional()
12543
12724
  });
12544
12725
  /**
12545
12726
  * One delivery-history row. This is a read-only VIEW over the durable
@@ -13046,21 +13227,32 @@ var ScopedTokenSchema = zod.z.object({
13046
13227
  * core, and no per-integration branch anywhere: `/api/oauth2/authorize` and
13047
13228
  * `/api/oauth2/integrations` are built from this collection alone.
13048
13229
  *
13049
- * **Scopes.** `requestedScopes` is baked into every token this integration is
13050
- * ever issued and the operator consents to it once. Derive it from the tRPC
13051
- * paths the client calls **with that token**, against `METHOD_ACCESS_MAP`, and
13052
- * prefer a narrow `capability:` scope to a `category:` one unless the client
13053
- * genuinely needs a whole family. A category scope grants every future member
13230
+ * **Scopes. `requestedScopes` has exactly ONE meaning: what the integration
13231
+ * NEEDS to function.** Not a blast radius, not a conservative
13232
+ * under-declaration, not a description of some other path the addon happens to
13233
+ * have. Derive it from what the client actually calls **with this token** —
13234
+ * every tRPC path against `METHOD_ACCESS_MAP`, plus an `addon:` grant for every
13235
+ * addon HTTP route it posts to — and write the call that justifies each entry
13236
+ * next to it. Two integrations once used this field to mean two different
13237
+ * things; the operator ruled there is one meaning, and any third integration
13238
+ * inherits it (2026-08-09).
13239
+ *
13240
+ * This is not documentation, it is the ENFORCEMENT INPUT. Since
13241
+ * [D103](../../../../docs/decisions/adr-0103.md) the `/addon/:addonId/*` gate
13242
+ * checks an integration token's grant before letting it reach an
13243
+ * `access: 'authenticated'` route, so an **under-declaration is an integration
13244
+ * that stops working** — a missing `addon:` entry means `403 Token scope
13245
+ * mismatch` on every control the client tries to actuate. Widen the descriptor
13246
+ * honestly rather than weakening a check to make a route pass.
13247
+ *
13248
+ * Prefer a narrow `capability:` scope to a `category:` one unless the client
13249
+ * genuinely needs a whole family; a category scope grants every future member
13054
13250
  * of that category too. `category:system [create]` has been rejected once and
13055
13251
  * should stay rejected: it hands `addons.installPackage` to an integration.
13056
13252
  *
13057
- * What it does NOT cover: calls the ADDON makes over `ctx.api`, which run as
13058
- * the addon and are not scope-checked. Alexa's descriptor is narrower than
13059
- * Home Assistant's for exactly that reason its Lambda posts directives and
13060
- * the addon does the work, while the Home Assistant component calls tRPC
13061
- * directly with the token. So `requestedScopes` describes the blast radius of
13062
- * the GRANT, not the reach of the integration; do not widen one to describe the
13063
- * other.
13253
+ * Calls the ADDON itself makes over `ctx.api` run as the addon and are not
13254
+ * scope-checked, so they are not what this field describes but reaching the
13255
+ * addon's route in the first place IS, and that is the entry to declare.
13064
13256
  *
13065
13257
  * **The boot window.** An addon registers its provider after its runner forks
13066
13258
  * and initialises, so between hub start and that moment this collection is
@@ -13101,7 +13293,30 @@ var OauthIntegrationDescriptorSchema = zod.z.object({
13101
13293
  * present, /api/oauth2/authorize bakes THIS into the code instead of the
13102
13294
  * hub-global `publicHubUrl()`, so a forked exporter addon (which can't set
13103
13295
  * the hub's env) drives the claim that its cloud Lambda routes back on. */
13104
- hubUrl: zod.z.string().optional()
13296
+ hubUrl: zod.z.string().optional(),
13297
+ /**
13298
+ * How long a REFRESH token issued for this integration lives — seconds, or
13299
+ * `'never'` for a token minted with no `exp` claim at all. Omit to keep the
13300
+ * 30-day default, which is what every link used before this field existed.
13301
+ *
13302
+ * Declared here for the same reason `requestedScopes` is: the integration
13303
+ * knows what it needs. Amazon's account linking and a Home Assistant config
13304
+ * entry are both meant to survive indefinitely, and re-linking is a manual
13305
+ * user action, so a 30-day expiry silently unlinks a working integration.
13306
+ *
13307
+ * **The security posture, stated so it is owned deliberately.** A refresh
13308
+ * token that never expires is permanent access if it leaks. What bounds it is
13309
+ * revocation, not time: `oauthRefresh` re-reads the session on every use and
13310
+ * returns `null` once `revokedAt` is set, as does `oauthVerifyAccessToken`.
13311
+ * The one gap is the ACCESS token — it is a plain signed JWT that nothing
13312
+ * re-checks against the session on the `/trpc` and `/addon/*` paths, so
13313
+ * revoking a link takes effect there only after its remaining hour. That hour
13314
+ * is why the access TTL is not configurable.
13315
+ *
13316
+ * The value is baked into the authorization code at `/authorize` and travels
13317
+ * on the tokens, so editing this field changes FUTURE links only.
13318
+ */
13319
+ refreshTokenTtlSec: zod.z.union([zod.z.number().int().positive(), zod.z.literal("never")]).optional()
13105
13320
  });
13106
13321
  var oauthIntegrationCapability = {
13107
13322
  name: "oauth-integration",
@@ -17616,7 +17831,16 @@ var SsoBridgeClaimsSchema = zod.z.object({
17616
17831
  codeChallenge: zod.z.string().optional(),
17617
17832
  /** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
17618
17833
  * tokens so the verify path can check the session is not revoked. */
17619
- sessionId: zod.z.string().optional()
17834
+ sessionId: zod.z.string().optional(),
17835
+ /**
17836
+ * The refresh lifetime this LINK was created with, in seconds, or `'never'`.
17837
+ * Baked into the code at `/authorize` from the integration's descriptor and
17838
+ * carried forward so `oauthRefresh` re-mints with the same lifetime. It rides
17839
+ * on the token rather than being re-read from the descriptor on purpose:
17840
+ * editing a descriptor must not retroactively extend or shorten a link the
17841
+ * operator already consented to.
17842
+ */
17843
+ refreshTtl: zod.z.union([zod.z.number().int().positive(), zod.z.literal("never")]).optional()
17620
17844
  });
17621
17845
  var ssoBridgeCapability = {
17622
17846
  name: "sso-bridge",
@@ -17626,7 +17850,16 @@ var ssoBridgeCapability = {
17626
17850
  methods: {
17627
17851
  signBridgeToken: require_sleep.method(zod.z.object({
17628
17852
  claims: SsoBridgeClaimsSchema,
17629
- ttlSec: zod.z.number().int().positive().optional()
17853
+ /**
17854
+ * Seconds, or `'never'` for a token minted with NO `exp` claim.
17855
+ *
17856
+ * `'never'` is a literal rather than `undefined`/`0` because omitting
17857
+ * this field already means "the 5-minute SSO hand-off default", and
17858
+ * `jwt.sign` THROWS on `{ expiresIn: undefined }` — a "no expiry" that
17859
+ * went through the numeric path would fail at mint time and break
17860
+ * linking rather than produce an eternal token.
17861
+ */
17862
+ ttlSec: zod.z.union([zod.z.number().int().positive(), zod.z.literal("never")]).optional()
17630
17863
  }), zod.z.object({ token: zod.z.string() })),
17631
17864
  verifyBridgeToken: require_sleep.method(zod.z.object({ token: zod.z.string() }), SsoBridgeClaimsSchema.nullable())
17632
17865
  }
@@ -26708,7 +26941,12 @@ var userManagementCapability = {
26708
26941
  hubUrl: zod.z.string(),
26709
26942
  /** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
26710
26943
  * that carries one can ONLY be exchanged with the matching verifier. */
26711
- codeChallenge: zod.z.string().optional()
26944
+ codeChallenge: zod.z.string().optional(),
26945
+ /** The integration's declared refresh lifetime — seconds, or `'never'`.
26946
+ * From `OauthIntegrationDescriptor.refreshTokenTtlSec`. Baked into the
26947
+ * code so the link carries its own lifetime; omit for the 30-day
26948
+ * default. */
26949
+ refreshTtlSec: zod.z.union([zod.z.number().int().positive(), zod.z.literal("never")]).optional()
26712
26950
  }), zod.z.object({ code: zod.z.string() }), {
26713
26951
  kind: "mutation",
26714
26952
  access: "create"
@@ -40130,6 +40368,8 @@ exports.NcSnoozeInputSchema = NcSnoozeInputSchema;
40130
40368
  exports.NcSnoozeSchema = NcSnoozeSchema;
40131
40369
  exports.NcSnoozeScopeSchema = NcSnoozeScopeSchema;
40132
40370
  exports.NcSnoozeSuppressedSchema = NcSnoozeSuppressedSchema;
40371
+ exports.NcSystemEventConditionSchema = NcSystemEventConditionSchema;
40372
+ exports.NcSystemEventKindSchema = NcSystemEventKindSchema;
40133
40373
  exports.NcTaxonomyEntrySchema = NcTaxonomyEntrySchema;
40134
40374
  exports.NcTaxonomySchema = NcTaxonomySchema;
40135
40375
  exports.NcTestResultSchema = NcTestResultSchema;
@@ -40501,6 +40741,7 @@ exports.canConvertUnit = canConvertUnit;
40501
40741
  exports.canonicalEgressPlan = canonicalEgressPlan;
40502
40742
  exports.carbonMonoxideCapability = carbonMonoxideCapability;
40503
40743
  exports.cellsToRects = cellsToRects;
40744
+ exports.classifyBearerPrincipal = classifyBearerPrincipal;
40504
40745
  exports.classifyStream = classifyStream;
40505
40746
  exports.classifyStreams = classifyStreams;
40506
40747
  exports.climateControlCapability = climateControlCapability;
@@ -40647,6 +40888,7 @@ exports.mediaPlayerCapability = mediaPlayerCapability;
40647
40888
  exports.mergeSourceInfo = mergeSourceInfo;
40648
40889
  exports.meshNetworkCapability = meshNetworkCapability;
40649
40890
  exports.method = require_sleep.method;
40891
+ exports.methodAccessForHttpMethod = methodAccessForHttpMethod;
40650
40892
  exports.metricsProviderCapability = metricsProviderCapability;
40651
40893
  exports.modelConvertCapability = modelConvertCapability;
40652
40894
  exports.modelDistributorCapability = modelDistributorCapability;
@@ -40695,6 +40937,7 @@ exports.powerMeterCapability = powerMeterCapability;
40695
40937
  exports.prepareNotification = prepareNotification;
40696
40938
  exports.presenceCapability = presenceCapability;
40697
40939
  exports.pressureSensorCapability = pressureSensorCapability;
40940
+ exports.principalMayReachAddon = principalMayReachAddon;
40698
40941
  exports.privacyMaskCapability = privacyMaskCapability;
40699
40942
  exports.procedureAuthKey = procedureAuthKey;
40700
40943
  exports.ptzAutotrackCapability = ptzAutotrackCapability;
@@ -40730,6 +40973,8 @@ exports.runInferenceStep = runInferenceStep;
40730
40973
  exports.runtimeDevices = runtimeDevices;
40731
40974
  exports.sceneMonitorCapability = sceneMonitorCapability;
40732
40975
  exports.scopeKey = require_sleep.scopeKey;
40976
+ exports.scopesAllowAddon = scopesAllowAddon;
40977
+ exports.scopesAllowDeviceCap = scopesAllowDeviceCap;
40733
40978
  exports.scoreRuntimes = scoreRuntimes;
40734
40979
  exports.scriptRunnerCapability = scriptRunnerCapability;
40735
40980
  exports.selectAssignedProfileSlots = require_sleep.selectAssignedProfileSlots;