@camstack/types 1.2.51 → 1.2.52
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/addon.js +1 -1
- package/dist/addon.mjs +1 -1
- package/dist/capabilities/snapshot.cap.d.ts +56 -0
- package/dist/generated/addon-api.d.ts +7 -0
- package/dist/generated/method-access-map.d.ts +1 -1
- package/dist/index.js +78 -1
- package/dist/index.mjs +78 -1
- package/dist/node.d.ts +2 -0
- package/dist/node.js +57 -0
- package/dist/node.mjs +57 -2
- package/dist/{sleep-7WqNZVcL.mjs → sleep-CT4lN1ij.mjs} +1 -0
- package/dist/{sleep-ocMLM2o5.js → sleep-DfF6vKCf.js} +1 -0
- package/dist/utils/expiring-url-signature.d.ts +27 -0
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_event_category = require("./event-category-BE4PDZ_3.js");
|
|
3
|
-
const require_sleep = require("./sleep-
|
|
3
|
+
const require_sleep = require("./sleep-DfF6vKCf.js");
|
|
4
4
|
const require_err_msg = require("./err-msg-COpsHMw2.js");
|
|
5
5
|
//#region src/generated/collection-array-methods.ts
|
|
6
6
|
/**
|
package/dist/addon.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as EventCategory } from "./event-category-41fKf-q9.mjs";
|
|
2
|
-
import { C as DeviceType, F as readNodePin, L as ReadinessRegistry, O as expandCapMethods, P as nodePin, R as ReadinessTimeoutError, V as scopeKey, a as asJsonObject, d as DEVICE_SCOPED_CAPS, f as isDeviceScopedCap, gt as normalizeAddonInitResult, ht as BaseAddon, mt as DisposerChain, p as createDeviceProxy, pt as DATAPLANE_SECRET_HEADER, s as asString, t as sleep, u as parseJsonUnknown, v as deviceOpsCapability, w as adminUiCapability, y as viewerUiCapability, yt as emitReadiness } from "./sleep-
|
|
2
|
+
import { C as DeviceType, F as readNodePin, L as ReadinessRegistry, O as expandCapMethods, P as nodePin, R as ReadinessTimeoutError, V as scopeKey, a as asJsonObject, d as DEVICE_SCOPED_CAPS, f as isDeviceScopedCap, gt as normalizeAddonInitResult, ht as BaseAddon, mt as DisposerChain, p as createDeviceProxy, pt as DATAPLANE_SECRET_HEADER, s as asString, t as sleep, u as parseJsonUnknown, v as deviceOpsCapability, w as adminUiCapability, y as viewerUiCapability, yt as emitReadiness } from "./sleep-CT4lN1ij.mjs";
|
|
3
3
|
import { t as errMsg } from "./err-msg-IQTHeDzc.mjs";
|
|
4
4
|
//#region src/generated/collection-array-methods.ts
|
|
5
5
|
/**
|
|
@@ -82,6 +82,62 @@ export declare const snapshotCapability: {
|
|
|
82
82
|
}, z.core.$strip>>, import("./capability-definition.js").CapabilityMethodKind> & {
|
|
83
83
|
readonly systemOnly: true;
|
|
84
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* Signed, expiring links to a CLIENT-SIZED frame — and the demand signal
|
|
87
|
+
* that makes those frames current.
|
|
88
|
+
*
|
|
89
|
+
* ## The problem it replaces
|
|
90
|
+
*
|
|
91
|
+
* `getSnapshotOverview` is cache-only by contract: it answers from whatever
|
|
92
|
+
* the wrapper happens to hold and never captures. Under D93 the client
|
|
93
|
+
* versions its image URL on that answer, and an image REQUEST is what enrols
|
|
94
|
+
* a camera in the keep-warm loop. Both of those are satisfiable by the
|
|
95
|
+
* client's own image cache — `expo-image` is URL-keyed and never revalidates
|
|
96
|
+
* — so a URL painted in a previous session comes off disk with no network,
|
|
97
|
+
* no enrolment, and nothing warming. Measured on the live hub: reopening
|
|
98
|
+
* after two minutes idle painted 15 of 16 tiles at **168 s old** with zero
|
|
99
|
+
* HTTP requests, and the fleet only recovered because a later poll happened
|
|
100
|
+
* to observe a different identity.
|
|
101
|
+
*
|
|
102
|
+
* ## The two properties that fix it
|
|
103
|
+
*
|
|
104
|
+
* **It is an RPC, so no client cache can answer it.** The demand signal
|
|
105
|
+
* always reaches the wrapper. This method therefore MAY create keep-warm
|
|
106
|
+
* subscriptions, where `getSnapshotOverview` must never (D93) — the
|
|
107
|
+
* distinction is not "one is newer" but that the overview poll is app-wide
|
|
108
|
+
* (a creating overview would warm every camera on the install) while this is
|
|
109
|
+
* called by a rendered surface naming the tiles it is actually painting, at
|
|
110
|
+
* the width it is painting them.
|
|
111
|
+
*
|
|
112
|
+
* **It waits, briefly and boundedly, for the capture it triggered.** The
|
|
113
|
+
* returned `capturedAt` is the frame the link will serve, not the frame the
|
|
114
|
+
* cache held when the client asked, so a first paint is honest and current
|
|
115
|
+
* instead of a generation behind. A device that does not settle inside the
|
|
116
|
+
* bound still gets a link and its real (older) `capturedAt` — the next poll
|
|
117
|
+
* carries it forward.
|
|
118
|
+
*
|
|
119
|
+
* `force` is never set on behalf of a client here. A sleeping battery camera
|
|
120
|
+
* is reported with `sleeping: true` and the last frame it produced, however
|
|
121
|
+
* old; the wrapper's existing sleep gate owns that decision and this method
|
|
122
|
+
* adds no second one.
|
|
123
|
+
*/
|
|
124
|
+
readonly getSnapshotLinks: import("./capability-definition.js").CapabilityMethodSchema<z.ZodObject<{
|
|
125
|
+
targets: z.ZodArray<z.ZodObject<{
|
|
126
|
+
deviceId: z.ZodNumber;
|
|
127
|
+
width: z.ZodOptional<z.ZodNumber>;
|
|
128
|
+
}, z.core.$strip>>;
|
|
129
|
+
}, z.core.$strip>, z.ZodArray<z.ZodObject<{
|
|
130
|
+
deviceId: z.ZodNumber;
|
|
131
|
+
url: z.ZodNullable<z.ZodString>;
|
|
132
|
+
capturedAt: z.ZodNullable<z.ZodNumber>;
|
|
133
|
+
ageMs: z.ZodNullable<z.ZodNumber>;
|
|
134
|
+
expiresAt: z.ZodNullable<z.ZodNumber>;
|
|
135
|
+
width: z.ZodNullable<z.ZodNumber>;
|
|
136
|
+
neverCaptured: z.ZodBoolean;
|
|
137
|
+
sleeping: z.ZodBoolean;
|
|
138
|
+
}, z.core.$strip>>, import("./capability-definition.js").CapabilityMethodKind> & {
|
|
139
|
+
readonly systemOnly: true;
|
|
140
|
+
};
|
|
85
141
|
};
|
|
86
142
|
readonly status: {
|
|
87
143
|
readonly schema: z.ZodObject<{
|
|
@@ -6575,6 +6575,13 @@ export type AppRouter = TrpcCoreRouter<{
|
|
|
6575
6575
|
output: z.infer<typeof snapshotCapability.methods.getSnapshotOverview.output>;
|
|
6576
6576
|
meta: object;
|
|
6577
6577
|
}>;
|
|
6578
|
+
getSnapshotLinks: TRPCQueryProcedure<{
|
|
6579
|
+
input: {
|
|
6580
|
+
[x: string]: unknown;
|
|
6581
|
+
} & z.input<typeof snapshotCapability.methods.getSnapshotLinks.input>;
|
|
6582
|
+
output: z.infer<typeof snapshotCapability.methods.getSnapshotLinks.output>;
|
|
6583
|
+
meta: object;
|
|
6584
|
+
}>;
|
|
6578
6585
|
}>>;
|
|
6579
6586
|
ssoBridge: TRPCBuiltRouter<{
|
|
6580
6587
|
ctx: TrpcContext;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* scope+access check inside `protectedProcedure` (see
|
|
7
7
|
* `server/backend/src/api/trpc/trpc.middleware.ts`).
|
|
8
8
|
*
|
|
9
|
-
* Coverage:
|
|
9
|
+
* Coverage: 901 method paths across 121 capabilities.
|
|
10
10
|
*/
|
|
11
11
|
import type { CapabilityMethodAccess } from '../capabilities/capability-definition.js';
|
|
12
12
|
export interface MethodAccessRecord {
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_event_category = require("./event-category-BE4PDZ_3.js");
|
|
3
|
-
const require_sleep = require("./sleep-
|
|
3
|
+
const require_sleep = require("./sleep-DfF6vKCf.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");
|
|
@@ -17308,6 +17308,77 @@ var snapshotCapability = {
|
|
|
17308
17308
|
lastCapturedAt: zod.z.number().nullable(),
|
|
17309
17309
|
cacheAgeMs: zod.z.number().nullable(),
|
|
17310
17310
|
etag: zod.z.string().nullable()
|
|
17311
|
+
}))),
|
|
17312
|
+
/**
|
|
17313
|
+
* Signed, expiring links to a CLIENT-SIZED frame — and the demand signal
|
|
17314
|
+
* that makes those frames current.
|
|
17315
|
+
*
|
|
17316
|
+
* ## The problem it replaces
|
|
17317
|
+
*
|
|
17318
|
+
* `getSnapshotOverview` is cache-only by contract: it answers from whatever
|
|
17319
|
+
* the wrapper happens to hold and never captures. Under D93 the client
|
|
17320
|
+
* versions its image URL on that answer, and an image REQUEST is what enrols
|
|
17321
|
+
* a camera in the keep-warm loop. Both of those are satisfiable by the
|
|
17322
|
+
* client's own image cache — `expo-image` is URL-keyed and never revalidates
|
|
17323
|
+
* — so a URL painted in a previous session comes off disk with no network,
|
|
17324
|
+
* no enrolment, and nothing warming. Measured on the live hub: reopening
|
|
17325
|
+
* after two minutes idle painted 15 of 16 tiles at **168 s old** with zero
|
|
17326
|
+
* HTTP requests, and the fleet only recovered because a later poll happened
|
|
17327
|
+
* to observe a different identity.
|
|
17328
|
+
*
|
|
17329
|
+
* ## The two properties that fix it
|
|
17330
|
+
*
|
|
17331
|
+
* **It is an RPC, so no client cache can answer it.** The demand signal
|
|
17332
|
+
* always reaches the wrapper. This method therefore MAY create keep-warm
|
|
17333
|
+
* subscriptions, where `getSnapshotOverview` must never (D93) — the
|
|
17334
|
+
* distinction is not "one is newer" but that the overview poll is app-wide
|
|
17335
|
+
* (a creating overview would warm every camera on the install) while this is
|
|
17336
|
+
* called by a rendered surface naming the tiles it is actually painting, at
|
|
17337
|
+
* the width it is painting them.
|
|
17338
|
+
*
|
|
17339
|
+
* **It waits, briefly and boundedly, for the capture it triggered.** The
|
|
17340
|
+
* returned `capturedAt` is the frame the link will serve, not the frame the
|
|
17341
|
+
* cache held when the client asked, so a first paint is honest and current
|
|
17342
|
+
* instead of a generation behind. A device that does not settle inside the
|
|
17343
|
+
* bound still gets a link and its real (older) `capturedAt` — the next poll
|
|
17344
|
+
* carries it forward.
|
|
17345
|
+
*
|
|
17346
|
+
* `force` is never set on behalf of a client here. A sleeping battery camera
|
|
17347
|
+
* is reported with `sleeping: true` and the last frame it produced, however
|
|
17348
|
+
* old; the wrapper's existing sleep gate owns that decision and this method
|
|
17349
|
+
* adds no second one.
|
|
17350
|
+
*/
|
|
17351
|
+
getSnapshotLinks: require_sleep.systemMethod(zod.z.object({
|
|
17352
|
+
/** The tiles a surface is actually rendering. One entry per (device,
|
|
17353
|
+
* width) the caller will paint — the width is snapped to the server's
|
|
17354
|
+
* ladder and becomes part of the link's SIGNED identity. */
|
|
17355
|
+
targets: zod.z.array(zod.z.object({
|
|
17356
|
+
deviceId: zod.z.number(),
|
|
17357
|
+
/** Target width in px. Omit for the frame as captured — correct
|
|
17358
|
+
* for a full-bleed surface, wrong (and expensive) for a grid. */
|
|
17359
|
+
width: zod.z.number().int().positive().optional()
|
|
17360
|
+
})).min(1).max(200) }), zod.z.array(zod.z.object({
|
|
17361
|
+
deviceId: zod.z.number(),
|
|
17362
|
+
/** Root-relative signed path, or null when the link plane is not
|
|
17363
|
+
* served (no data-plane facility). Present even for a device that has
|
|
17364
|
+
* never captured — the request is what triggers the first one (D94). */
|
|
17365
|
+
url: zod.z.string().nullable(),
|
|
17366
|
+
/** Epoch ms of the frame this link serves. Null = never captured.
|
|
17367
|
+
* THE honest age: the tRPC path carried none before this. */
|
|
17368
|
+
capturedAt: zod.z.number().nullable(),
|
|
17369
|
+
/** Age of that frame at the moment the answer was built. */
|
|
17370
|
+
ageMs: zod.z.number().nullable(),
|
|
17371
|
+
/** Epoch ms after which `url` stops verifying. */
|
|
17372
|
+
expiresAt: zod.z.number().nullable(),
|
|
17373
|
+
/** Ladder rung the bytes are at; null = the frame as captured. */
|
|
17374
|
+
width: zod.z.number().nullable(),
|
|
17375
|
+
/** The device has never produced a frame. An empty state, not a
|
|
17376
|
+
* failure — and never a reason to withhold the link (D94). */
|
|
17377
|
+
neverCaptured: zod.z.boolean(),
|
|
17378
|
+
/** A sleeping battery camera: the frame is deliberately stale and will
|
|
17379
|
+
* NOT refresh in the background. A surface should say so rather than
|
|
17380
|
+
* present it as current. */
|
|
17381
|
+
sleeping: zod.z.boolean()
|
|
17311
17382
|
})))
|
|
17312
17383
|
},
|
|
17313
17384
|
status: {
|
|
@@ -34981,6 +35052,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
34981
35052
|
addonId: null,
|
|
34982
35053
|
access: "view"
|
|
34983
35054
|
},
|
|
35055
|
+
"snapshot.getSnapshotLinks": {
|
|
35056
|
+
capName: "snapshot",
|
|
35057
|
+
capScope: "device",
|
|
35058
|
+
addonId: null,
|
|
35059
|
+
access: "view"
|
|
35060
|
+
},
|
|
34984
35061
|
"snapshot.getSnapshotOverview": {
|
|
34985
35062
|
capName: "snapshot",
|
|
34986
35063
|
capScope: "device",
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as EventCategory } from "./event-category-41fKf-q9.mjs";
|
|
2
|
-
import { $ as FrameHandleSchema, A as method, B as readinessKey, C as DeviceType, Ct as collectHydratedFieldEntries, D as event, E as DEVICE_STATUS_METHOD, Et as resolveHydratedFieldValue, F as readNodePin, G as CamProfileSchema, H as BrokerStatsSchema, I as toNodeId, J as CameraStreamSchema, K as CamStreamKindSchema, L as ReadinessRegistry, M as systemMethod, N as CAP_NODE_PIN_CONTEXT_KEY, O as expandCapMethods, P as nodePin, Q as FrameHandleFormatSchema, R as ReadinessTimeoutError, S as DeviceRole, St as WELL_KNOWN_TAB_MAP, T as DEVICE_SETTINGS_CONTRIBUTION_METHODS, Tt as hydrateSchema, U as BrokerStatusSchema, V as scopeKey, W as CAM_PROFILE_ORDER, X as DecodedFrameSchema, Y as DecodedAudioChunkSchema, Z as EncodedPacketSchema, _ as RawStateResultSchema, _t as createDurableState, a as asJsonObject, at as SubscribeAudioChunksInputSchema, b as ChargingStatus, bt as isEvent, c as parseJsonArray, ct as SubscribeFramesResultSchema, d as DEVICE_SCOPED_CAPS, dt as parseProfileBrokerId, et as ProfileRtspEntrySchema, f as isDeviceScopedCap, ft as selectAssignedProfileSlots, g as createSliceHandle, gt as normalizeAddonInitResult, h as createMirrorSource, ht as BaseAddon, i as asJsonArray, it as StreamSourceSchema, j as resolveCapMount, k as isDeviceConfigCap, l as parseJsonObject, lt as makeProfileBrokerId, m as createLazyTrpcSource, mt as DisposerChain, n as sleepCancellable, nt as ProfileSlotStatusSchema, o as asNumber, ot as SubscribeAudioChunksResultSchema, p as createDeviceProxy, pt as DATAPLANE_SECRET_HEADER, q as CamStreamResolutionSchema, r as asBoolean, rt as StreamSourceEntrySchema, s as asString, st as SubscribeFramesInputSchema, t as sleep, tt as ProfileSlotSchema, u as parseJsonUnknown, ut as makeSourceBrokerId, v as deviceOpsCapability, vt as createEvent, w as adminUiCapability, wt as collectHydratedFieldValues, x as DeviceFeature, xt as WELL_KNOWN_TABS, y as viewerUiCapability, yt as emitReadiness, z as emitDownForOwnedCaps } from "./sleep-
|
|
2
|
+
import { $ as FrameHandleSchema, A as method, B as readinessKey, C as DeviceType, Ct as collectHydratedFieldEntries, D as event, E as DEVICE_STATUS_METHOD, Et as resolveHydratedFieldValue, F as readNodePin, G as CamProfileSchema, H as BrokerStatsSchema, I as toNodeId, J as CameraStreamSchema, K as CamStreamKindSchema, L as ReadinessRegistry, M as systemMethod, N as CAP_NODE_PIN_CONTEXT_KEY, O as expandCapMethods, P as nodePin, Q as FrameHandleFormatSchema, R as ReadinessTimeoutError, S as DeviceRole, St as WELL_KNOWN_TAB_MAP, T as DEVICE_SETTINGS_CONTRIBUTION_METHODS, Tt as hydrateSchema, U as BrokerStatusSchema, V as scopeKey, W as CAM_PROFILE_ORDER, X as DecodedFrameSchema, Y as DecodedAudioChunkSchema, Z as EncodedPacketSchema, _ as RawStateResultSchema, _t as createDurableState, a as asJsonObject, at as SubscribeAudioChunksInputSchema, b as ChargingStatus, bt as isEvent, c as parseJsonArray, ct as SubscribeFramesResultSchema, d as DEVICE_SCOPED_CAPS, dt as parseProfileBrokerId, et as ProfileRtspEntrySchema, f as isDeviceScopedCap, ft as selectAssignedProfileSlots, g as createSliceHandle, gt as normalizeAddonInitResult, h as createMirrorSource, ht as BaseAddon, i as asJsonArray, it as StreamSourceSchema, j as resolveCapMount, k as isDeviceConfigCap, l as parseJsonObject, lt as makeProfileBrokerId, m as createLazyTrpcSource, mt as DisposerChain, n as sleepCancellable, nt as ProfileSlotStatusSchema, o as asNumber, ot as SubscribeAudioChunksResultSchema, p as createDeviceProxy, pt as DATAPLANE_SECRET_HEADER, q as CamStreamResolutionSchema, r as asBoolean, rt as StreamSourceEntrySchema, s as asString, st as SubscribeFramesInputSchema, t as sleep, tt as ProfileSlotSchema, u as parseJsonUnknown, ut as makeSourceBrokerId, v as deviceOpsCapability, vt as createEvent, w as adminUiCapability, wt as collectHydratedFieldValues, x as DeviceFeature, xt as WELL_KNOWN_TABS, y as viewerUiCapability, yt as emitReadiness, z as emitDownForOwnedCaps } from "./sleep-CT4lN1ij.mjs";
|
|
3
3
|
import { a as buildAudioArgs, c as buildVideoArgs, d as logBannerArgs, f as pickVideoEncoder, i as audioPlanFromEncodeProfile, l as invocationFromEncodeProfile, n as Fmp4BoxSplitter, o as buildFfmpegArgs, r as AUDIO_PRESETS, s as buildInputArgs, t as canonicalHash, u as isSoftwareDecode } from "./canonical-hash-rO1sRmEK.mjs";
|
|
4
4
|
import { EventSourceType } from "./enums.mjs";
|
|
5
5
|
import { t as errMsg } from "./err-msg-IQTHeDzc.mjs";
|
|
@@ -17307,6 +17307,77 @@ var snapshotCapability = {
|
|
|
17307
17307
|
lastCapturedAt: z.number().nullable(),
|
|
17308
17308
|
cacheAgeMs: z.number().nullable(),
|
|
17309
17309
|
etag: z.string().nullable()
|
|
17310
|
+
}))),
|
|
17311
|
+
/**
|
|
17312
|
+
* Signed, expiring links to a CLIENT-SIZED frame — and the demand signal
|
|
17313
|
+
* that makes those frames current.
|
|
17314
|
+
*
|
|
17315
|
+
* ## The problem it replaces
|
|
17316
|
+
*
|
|
17317
|
+
* `getSnapshotOverview` is cache-only by contract: it answers from whatever
|
|
17318
|
+
* the wrapper happens to hold and never captures. Under D93 the client
|
|
17319
|
+
* versions its image URL on that answer, and an image REQUEST is what enrols
|
|
17320
|
+
* a camera in the keep-warm loop. Both of those are satisfiable by the
|
|
17321
|
+
* client's own image cache — `expo-image` is URL-keyed and never revalidates
|
|
17322
|
+
* — so a URL painted in a previous session comes off disk with no network,
|
|
17323
|
+
* no enrolment, and nothing warming. Measured on the live hub: reopening
|
|
17324
|
+
* after two minutes idle painted 15 of 16 tiles at **168 s old** with zero
|
|
17325
|
+
* HTTP requests, and the fleet only recovered because a later poll happened
|
|
17326
|
+
* to observe a different identity.
|
|
17327
|
+
*
|
|
17328
|
+
* ## The two properties that fix it
|
|
17329
|
+
*
|
|
17330
|
+
* **It is an RPC, so no client cache can answer it.** The demand signal
|
|
17331
|
+
* always reaches the wrapper. This method therefore MAY create keep-warm
|
|
17332
|
+
* subscriptions, where `getSnapshotOverview` must never (D93) — the
|
|
17333
|
+
* distinction is not "one is newer" but that the overview poll is app-wide
|
|
17334
|
+
* (a creating overview would warm every camera on the install) while this is
|
|
17335
|
+
* called by a rendered surface naming the tiles it is actually painting, at
|
|
17336
|
+
* the width it is painting them.
|
|
17337
|
+
*
|
|
17338
|
+
* **It waits, briefly and boundedly, for the capture it triggered.** The
|
|
17339
|
+
* returned `capturedAt` is the frame the link will serve, not the frame the
|
|
17340
|
+
* cache held when the client asked, so a first paint is honest and current
|
|
17341
|
+
* instead of a generation behind. A device that does not settle inside the
|
|
17342
|
+
* bound still gets a link and its real (older) `capturedAt` — the next poll
|
|
17343
|
+
* carries it forward.
|
|
17344
|
+
*
|
|
17345
|
+
* `force` is never set on behalf of a client here. A sleeping battery camera
|
|
17346
|
+
* is reported with `sleeping: true` and the last frame it produced, however
|
|
17347
|
+
* old; the wrapper's existing sleep gate owns that decision and this method
|
|
17348
|
+
* adds no second one.
|
|
17349
|
+
*/
|
|
17350
|
+
getSnapshotLinks: systemMethod(z.object({
|
|
17351
|
+
/** The tiles a surface is actually rendering. One entry per (device,
|
|
17352
|
+
* width) the caller will paint — the width is snapped to the server's
|
|
17353
|
+
* ladder and becomes part of the link's SIGNED identity. */
|
|
17354
|
+
targets: z.array(z.object({
|
|
17355
|
+
deviceId: z.number(),
|
|
17356
|
+
/** Target width in px. Omit for the frame as captured — correct
|
|
17357
|
+
* for a full-bleed surface, wrong (and expensive) for a grid. */
|
|
17358
|
+
width: z.number().int().positive().optional()
|
|
17359
|
+
})).min(1).max(200) }), z.array(z.object({
|
|
17360
|
+
deviceId: z.number(),
|
|
17361
|
+
/** Root-relative signed path, or null when the link plane is not
|
|
17362
|
+
* served (no data-plane facility). Present even for a device that has
|
|
17363
|
+
* never captured — the request is what triggers the first one (D94). */
|
|
17364
|
+
url: z.string().nullable(),
|
|
17365
|
+
/** Epoch ms of the frame this link serves. Null = never captured.
|
|
17366
|
+
* THE honest age: the tRPC path carried none before this. */
|
|
17367
|
+
capturedAt: z.number().nullable(),
|
|
17368
|
+
/** Age of that frame at the moment the answer was built. */
|
|
17369
|
+
ageMs: z.number().nullable(),
|
|
17370
|
+
/** Epoch ms after which `url` stops verifying. */
|
|
17371
|
+
expiresAt: z.number().nullable(),
|
|
17372
|
+
/** Ladder rung the bytes are at; null = the frame as captured. */
|
|
17373
|
+
width: z.number().nullable(),
|
|
17374
|
+
/** The device has never produced a frame. An empty state, not a
|
|
17375
|
+
* failure — and never a reason to withhold the link (D94). */
|
|
17376
|
+
neverCaptured: z.boolean(),
|
|
17377
|
+
/** A sleeping battery camera: the frame is deliberately stale and will
|
|
17378
|
+
* NOT refresh in the background. A surface should say so rather than
|
|
17379
|
+
* present it as current. */
|
|
17380
|
+
sleeping: z.boolean()
|
|
17310
17381
|
})))
|
|
17311
17382
|
},
|
|
17312
17383
|
status: {
|
|
@@ -34980,6 +35051,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
34980
35051
|
addonId: null,
|
|
34981
35052
|
access: "view"
|
|
34982
35053
|
},
|
|
35054
|
+
"snapshot.getSnapshotLinks": {
|
|
35055
|
+
capName: "snapshot",
|
|
35056
|
+
capScope: "device",
|
|
35057
|
+
addonId: null,
|
|
35058
|
+
access: "view"
|
|
35059
|
+
},
|
|
34983
35060
|
"snapshot.getSnapshotOverview": {
|
|
34984
35061
|
capName: "snapshot",
|
|
34985
35062
|
capScope: "device",
|
package/dist/node.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export { ensureFfmpeg, getFfmpegDownloadUrl } from './deps/ffmpeg-downloader.js'
|
|
|
4
4
|
export { ensurePython, installPythonPackages, installPythonRequirements, getPythonDownloadUrl, PYTHON_VERSION, } from './deps/python-downloader.js';
|
|
5
5
|
export { FilesystemStorageProvider } from './storage/filesystem-storage-provider.js';
|
|
6
6
|
export { canonicalHash } from './utils/canonical-hash.js';
|
|
7
|
+
export { signExpiringUrl, verifyExpiringUrl } from './utils/expiring-url-signature.js';
|
|
8
|
+
export type { VerifyExpiringUrlInput } from './utils/expiring-url-signature.js';
|
|
7
9
|
export { canonicalDeviceFingerprint, diffExportTargets, resolveExportFingerprint, } from './utils/export-reconciler.js';
|
|
8
10
|
export type { DeviceExportShape, ExportDelta, ExportTargetEntry, } from './utils/export-reconciler.js';
|
|
9
11
|
export { FfmpegProcess } from './ffmpeg/process.js';
|
package/dist/node.js
CHANGED
|
@@ -601,6 +601,61 @@ var FilesystemStorageProvider = class {
|
|
|
601
601
|
}
|
|
602
602
|
};
|
|
603
603
|
//#endregion
|
|
604
|
+
//#region src/utils/expiring-url-signature.ts
|
|
605
|
+
/**
|
|
606
|
+
* The ONE derivation of "a signed, expiring URL".
|
|
607
|
+
*
|
|
608
|
+
* This repo mints unguessable, self-expiring links in three places now — the
|
|
609
|
+
* notification artifact plane, the Home Assistant media plane, and the snapshot
|
|
610
|
+
* link plane. The first two grew independently and are byte-identical logic
|
|
611
|
+
* (`hmac(secret, "<id>:<exp>")`, expiry checked before a constant-time compare),
|
|
612
|
+
* each restating the crypto locally because **addons never import each other**.
|
|
613
|
+
*
|
|
614
|
+
* That reason is real, and the conclusion drawn from it was wrong. Two copies of
|
|
615
|
+
* a signing scheme is how one of them quietly ends up with a different TTL, a
|
|
616
|
+
* different compare, or a missing expiry check, and nothing fails until a link
|
|
617
|
+
* that should have died keeps working. The fix is the same one D52 applies to
|
|
618
|
+
* crop geometry: one derivation, in a place every addon may depend on. A
|
|
619
|
+
* framework package is exactly that place — `@camstack/types/node`, off the root
|
|
620
|
+
* entry because `node:crypto` must never be traversed by a browser bundler.
|
|
621
|
+
*
|
|
622
|
+
* What this module deliberately does NOT decide: the TTL, the base URL, the
|
|
623
|
+
* shape of `id`, and the access level of the route. Those are per-plane policy
|
|
624
|
+
* and each caller states them where a reader can see them.
|
|
625
|
+
*/
|
|
626
|
+
/**
|
|
627
|
+
* The signature over `(id, expMs)`.
|
|
628
|
+
*
|
|
629
|
+
* `id` is whatever the plane uses to name the thing being served — an artifact
|
|
630
|
+
* id, a track id, a `<deviceId>:<width>` pair. It is joined with `:` so a caller
|
|
631
|
+
* must not put a `:` inside a field whose boundary matters; where a plane has
|
|
632
|
+
* more than one field, it composes them itself and owns that ambiguity.
|
|
633
|
+
*/
|
|
634
|
+
function signExpiringUrl(secret, id, expMs) {
|
|
635
|
+
return (0, node_crypto.createHmac)("sha256", secret).update(`${id}:${String(expMs)}`).digest("hex");
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Verify a request's `(id, exp, sig)`.
|
|
639
|
+
*
|
|
640
|
+
* **Expiry is checked BEFORE the compare**, so an expired link is refused
|
|
641
|
+
* whether or not its signature is valid — a leaked URL stops working on its own
|
|
642
|
+
* and cannot be kept alive by holding a correct signature. The compare itself is
|
|
643
|
+
* constant-time so a public route cannot be probed for the signature byte by
|
|
644
|
+
* byte.
|
|
645
|
+
*/
|
|
646
|
+
function verifyExpiringUrl(input) {
|
|
647
|
+
const { secret, id, exp, sig, nowMs } = input;
|
|
648
|
+
if (exp === void 0 || sig === void 0) return false;
|
|
649
|
+
const expMs = typeof exp === "number" ? exp : Number(exp);
|
|
650
|
+
if (!Number.isFinite(expMs)) return false;
|
|
651
|
+
if (expMs <= nowMs) return false;
|
|
652
|
+
const expected = signExpiringUrl(secret, id, expMs);
|
|
653
|
+
const a = Buffer.from(expected, "utf8");
|
|
654
|
+
const b = Buffer.from(sig, "utf8");
|
|
655
|
+
if (a.length !== b.length) return false;
|
|
656
|
+
return (0, node_crypto.timingSafeEqual)(a, b);
|
|
657
|
+
}
|
|
658
|
+
//#endregion
|
|
604
659
|
//#region src/utils/export-reconciler.ts
|
|
605
660
|
/**
|
|
606
661
|
* Compute the stable 64-char lowercase-hex fingerprint of a device's
|
|
@@ -1446,3 +1501,5 @@ exports.getPythonDownloadUrl = getPythonDownloadUrl;
|
|
|
1446
1501
|
exports.installPythonPackages = installPythonPackages;
|
|
1447
1502
|
exports.installPythonRequirements = installPythonRequirements;
|
|
1448
1503
|
exports.resolveExportFingerprint = resolveExportFingerprint;
|
|
1504
|
+
exports.signExpiringUrl = signExpiringUrl;
|
|
1505
|
+
exports.verifyExpiringUrl = verifyExpiringUrl;
|
package/dist/node.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as Fmp4BoxSplitter, o as buildFfmpegArgs, t as canonicalHash, u as isSoftwareDecode } from "./canonical-hash-rO1sRmEK.mjs";
|
|
2
2
|
import { t as errMsg } from "./err-msg-IQTHeDzc.mjs";
|
|
3
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
import { chmodSync, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
6
|
import * as path from "node:path";
|
|
@@ -578,6 +578,61 @@ var FilesystemStorageProvider = class {
|
|
|
578
578
|
}
|
|
579
579
|
};
|
|
580
580
|
//#endregion
|
|
581
|
+
//#region src/utils/expiring-url-signature.ts
|
|
582
|
+
/**
|
|
583
|
+
* The ONE derivation of "a signed, expiring URL".
|
|
584
|
+
*
|
|
585
|
+
* This repo mints unguessable, self-expiring links in three places now — the
|
|
586
|
+
* notification artifact plane, the Home Assistant media plane, and the snapshot
|
|
587
|
+
* link plane. The first two grew independently and are byte-identical logic
|
|
588
|
+
* (`hmac(secret, "<id>:<exp>")`, expiry checked before a constant-time compare),
|
|
589
|
+
* each restating the crypto locally because **addons never import each other**.
|
|
590
|
+
*
|
|
591
|
+
* That reason is real, and the conclusion drawn from it was wrong. Two copies of
|
|
592
|
+
* a signing scheme is how one of them quietly ends up with a different TTL, a
|
|
593
|
+
* different compare, or a missing expiry check, and nothing fails until a link
|
|
594
|
+
* that should have died keeps working. The fix is the same one D52 applies to
|
|
595
|
+
* crop geometry: one derivation, in a place every addon may depend on. A
|
|
596
|
+
* framework package is exactly that place — `@camstack/types/node`, off the root
|
|
597
|
+
* entry because `node:crypto` must never be traversed by a browser bundler.
|
|
598
|
+
*
|
|
599
|
+
* What this module deliberately does NOT decide: the TTL, the base URL, the
|
|
600
|
+
* shape of `id`, and the access level of the route. Those are per-plane policy
|
|
601
|
+
* and each caller states them where a reader can see them.
|
|
602
|
+
*/
|
|
603
|
+
/**
|
|
604
|
+
* The signature over `(id, expMs)`.
|
|
605
|
+
*
|
|
606
|
+
* `id` is whatever the plane uses to name the thing being served — an artifact
|
|
607
|
+
* id, a track id, a `<deviceId>:<width>` pair. It is joined with `:` so a caller
|
|
608
|
+
* must not put a `:` inside a field whose boundary matters; where a plane has
|
|
609
|
+
* more than one field, it composes them itself and owns that ambiguity.
|
|
610
|
+
*/
|
|
611
|
+
function signExpiringUrl(secret, id, expMs) {
|
|
612
|
+
return createHmac("sha256", secret).update(`${id}:${String(expMs)}`).digest("hex");
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Verify a request's `(id, exp, sig)`.
|
|
616
|
+
*
|
|
617
|
+
* **Expiry is checked BEFORE the compare**, so an expired link is refused
|
|
618
|
+
* whether or not its signature is valid — a leaked URL stops working on its own
|
|
619
|
+
* and cannot be kept alive by holding a correct signature. The compare itself is
|
|
620
|
+
* constant-time so a public route cannot be probed for the signature byte by
|
|
621
|
+
* byte.
|
|
622
|
+
*/
|
|
623
|
+
function verifyExpiringUrl(input) {
|
|
624
|
+
const { secret, id, exp, sig, nowMs } = input;
|
|
625
|
+
if (exp === void 0 || sig === void 0) return false;
|
|
626
|
+
const expMs = typeof exp === "number" ? exp : Number(exp);
|
|
627
|
+
if (!Number.isFinite(expMs)) return false;
|
|
628
|
+
if (expMs <= nowMs) return false;
|
|
629
|
+
const expected = signExpiringUrl(secret, id, expMs);
|
|
630
|
+
const a = Buffer.from(expected, "utf8");
|
|
631
|
+
const b = Buffer.from(sig, "utf8");
|
|
632
|
+
if (a.length !== b.length) return false;
|
|
633
|
+
return timingSafeEqual(a, b);
|
|
634
|
+
}
|
|
635
|
+
//#endregion
|
|
581
636
|
//#region src/utils/export-reconciler.ts
|
|
582
637
|
/**
|
|
583
638
|
* Compute the stable 64-char lowercase-hex fingerprint of a device's
|
|
@@ -1403,4 +1458,4 @@ var Fmp4FragmentChild = class {
|
|
|
1403
1458
|
}
|
|
1404
1459
|
};
|
|
1405
1460
|
//#endregion
|
|
1406
|
-
export { FfmpegProcess, FilesystemStorageProvider, Fmp4FragmentChild, Fmp4FragmentPlane, PYTHON_VERSION, buildBinaryPath, canonicalDeviceFingerprint, canonicalHash, diffExportTargets, downloadBinary, ensureBinary, ensureFfmpeg, ensurePython, findInPath, getFfmpegDownloadUrl, getPlatformInfo, getPythonDownloadUrl, installPythonPackages, installPythonRequirements, resolveExportFingerprint };
|
|
1461
|
+
export { FfmpegProcess, FilesystemStorageProvider, Fmp4FragmentChild, Fmp4FragmentPlane, PYTHON_VERSION, buildBinaryPath, canonicalDeviceFingerprint, canonicalHash, diffExportTargets, downloadBinary, ensureBinary, ensureFfmpeg, ensurePython, findInPath, getFfmpegDownloadUrl, getPlatformInfo, getPythonDownloadUrl, installPythonPackages, installPythonRequirements, resolveExportFingerprint, signExpiringUrl, verifyExpiringUrl };
|
|
@@ -3279,6 +3279,7 @@ function createDeviceProxy(api, binding, opts) {
|
|
|
3279
3279
|
getSnapshot: (input) => dispatch("snapshot", "snapshot", "getSnapshot", "query", input),
|
|
3280
3280
|
invalidateCache: (input) => dispatch("snapshot", "snapshot", "invalidateCache", "mutation", input),
|
|
3281
3281
|
getSnapshotOverview: (input) => dispatch("snapshot", "snapshot", "getSnapshotOverview", "query", input),
|
|
3282
|
+
getSnapshotLinks: (input) => dispatch("snapshot", "snapshot", "getSnapshotLinks", "query", input),
|
|
3282
3283
|
getStatus: (input) => dispatch("snapshot", "snapshot", "getStatus", "query", input),
|
|
3283
3284
|
getDeviceSettingsContribution: (input) => dispatch("snapshot", "snapshot", "getDeviceSettingsContribution", "query", input),
|
|
3284
3285
|
getDeviceLiveContribution: (input) => dispatch("snapshot", "snapshot", "getDeviceLiveContribution", "query", input),
|
|
@@ -3279,6 +3279,7 @@ function createDeviceProxy(api, binding, opts) {
|
|
|
3279
3279
|
getSnapshot: (input) => dispatch("snapshot", "snapshot", "getSnapshot", "query", input),
|
|
3280
3280
|
invalidateCache: (input) => dispatch("snapshot", "snapshot", "invalidateCache", "mutation", input),
|
|
3281
3281
|
getSnapshotOverview: (input) => dispatch("snapshot", "snapshot", "getSnapshotOverview", "query", input),
|
|
3282
|
+
getSnapshotLinks: (input) => dispatch("snapshot", "snapshot", "getSnapshotLinks", "query", input),
|
|
3282
3283
|
getStatus: (input) => dispatch("snapshot", "snapshot", "getStatus", "query", input),
|
|
3283
3284
|
getDeviceSettingsContribution: (input) => dispatch("snapshot", "snapshot", "getDeviceSettingsContribution", "query", input),
|
|
3284
3285
|
getDeviceLiveContribution: (input) => dispatch("snapshot", "snapshot", "getDeviceLiveContribution", "query", input),
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signature over `(id, expMs)`.
|
|
3
|
+
*
|
|
4
|
+
* `id` is whatever the plane uses to name the thing being served — an artifact
|
|
5
|
+
* id, a track id, a `<deviceId>:<width>` pair. It is joined with `:` so a caller
|
|
6
|
+
* must not put a `:` inside a field whose boundary matters; where a plane has
|
|
7
|
+
* more than one field, it composes them itself and owns that ambiguity.
|
|
8
|
+
*/
|
|
9
|
+
export declare function signExpiringUrl(secret: string, id: string, expMs: number): string;
|
|
10
|
+
export interface VerifyExpiringUrlInput {
|
|
11
|
+
readonly secret: string;
|
|
12
|
+
readonly id: string;
|
|
13
|
+
/** Raw query value — a string off the wire, or already a number. */
|
|
14
|
+
readonly exp: string | number | undefined;
|
|
15
|
+
readonly sig: string | undefined;
|
|
16
|
+
readonly nowMs: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Verify a request's `(id, exp, sig)`.
|
|
20
|
+
*
|
|
21
|
+
* **Expiry is checked BEFORE the compare**, so an expired link is refused
|
|
22
|
+
* whether or not its signature is valid — a leaked URL stops working on its own
|
|
23
|
+
* and cannot be kept alive by holding a correct signature. The compare itself is
|
|
24
|
+
* constant-time so a public route cannot be probed for the signature byte by
|
|
25
|
+
* byte.
|
|
26
|
+
*/
|
|
27
|
+
export declare function verifyExpiringUrl(input: VerifyExpiringUrlInput): boolean;
|