@thuzjq/meteorcloud-device-sdk-node 0.5.1

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/index.d.ts ADDED
@@ -0,0 +1,930 @@
1
+ // Type definitions for @thuzjq/meteorcloud-device-sdk-node 0.5.1
2
+ // MeteorCloud device SDK: account authorization + chunked direct-PUT uploads.
3
+ //
4
+ // 0.5.0 changes the authorization model, not the upload wire contract. The
5
+ // browser bind no longer relays a management-page grant and the consent page no
6
+ // longer picks cameras; instead every upload carries its own machine
7
+ // declaration (a `camera` block) and the server registers on first sight. The
8
+ // SDK holds no camera list, no station list and no key-to-uid map.
9
+ //
10
+ // Contract: docs/architecture/account-client-sdk-contract.zh-CN.md §1.2, §5.3,
11
+ // §6.1-§6.6, §7.6, §7.9; decisions in docs/ACCOUNT_CLIENT_SDK_PLAN.md §0.
12
+ //
13
+ // ⚠ ALPHA. The server endpoints these types describe (`/account`, `/stations`,
14
+ // `/cameras`, `/cameras/ensure`, the owner-chain authorizer, zero-binding
15
+ // registration, the browser login handoff) are NOT built. See
16
+ // docs/NODE_INTEGRATION_GUIDE.zh-CN.md §0.
17
+
18
+ export type ArtifactRole = 'manifest' | 'ecsv' | 'media' | 'preview';
19
+
20
+ export type ProductClientId = 'meteormasterai' | 'meteorstudio' | 'ufocapture-adapter';
21
+
22
+ export type UploadStage = 'hashing' | 'authorizing' | 'uploading' | 'finalizing' | 'completed';
23
+
24
+ export type ErrorKind =
25
+ | 'validation'
26
+ | 'io'
27
+ | 'transport'
28
+ | 'device_api'
29
+ | 'auth'
30
+ | 'keystore'
31
+ | 'cancelled';
32
+
33
+ /**
34
+ * `key_reference` URI schemes, weakest to strongest.
35
+ *
36
+ * Spelled the same as in the C++ SDK: the installation config is a cross-SDK
37
+ * wire contract, so a config written by one has to be readable by the other.
38
+ * (`windows-cng` appeared in 0.3-era docs as a reserved name and was never
39
+ * implemented under that spelling; it is rejected with a pointer to `cng`.)
40
+ */
41
+ export type KeyReferenceScheme = 'file' | 'dpapi' | 'cng';
42
+
43
+ export interface Artifact {
44
+ role: ArtifactRole;
45
+ path: string;
46
+ contentType: string;
47
+ }
48
+
49
+ export interface Progress {
50
+ stage: UploadStage;
51
+ role?: ArtifactRole;
52
+ transferredBytes?: number;
53
+ totalBytes?: number;
54
+ }
55
+
56
+ /** Returning `false` cancels the upload with a `cancelled` error. */
57
+ export type ProgressCallback = (progress: Progress) => boolean | void | Promise<boolean | void>;
58
+
59
+ export interface SafeError {
60
+ name: string;
61
+ kind: ErrorKind;
62
+ httpStatus?: number;
63
+ apiCode?: number;
64
+ oauthError?: string;
65
+ retryable: boolean;
66
+ retryAfterSeconds?: number;
67
+ action?: string;
68
+ message: string;
69
+ }
70
+
71
+ export declare class MeteorCloudError extends Error {
72
+ readonly kind: ErrorKind;
73
+ readonly httpStatus?: number;
74
+ /** Device API stable business code (the `code` member of the response envelope). */
75
+ readonly apiCode?: number;
76
+ /** RFC 6749 `error` value when the failure came from the token endpoint. */
77
+ readonly oauthError?: string;
78
+ readonly retryable: boolean;
79
+ readonly retryAfterSeconds?: number;
80
+ /**
81
+ * Which step failed, when the step is worth branching on. The bind flow
82
+ * emits `authorize_timeout` (nobody came back — retryable by asking the user
83
+ * again), `authorize_cancelled` and `authorize_aborted` (a deliberate stop —
84
+ * not retryable on its own), `open_browser` and `loopback_listen`.
85
+ *
86
+ * Two are worth special-casing, and both mean the bind SUCCEEDED:
87
+ *
88
+ * - `account_api_unsupported` — the server does not route the account-level
89
+ * Device API at all. Upgrade the server; never bind again.
90
+ * - `bind_registered_load_config` — the installation is registered and the
91
+ * config is written, but the first token or the account read failed. Load
92
+ * the config named by {@link MeteorCloudError.configPath} instead of
93
+ * calling `connect()` again, which would mint a second key, register a
94
+ * second installation and strand this one. The C++ SDK raises the same
95
+ * identifier for the same window; the string is the cross-language
96
+ * contract.
97
+ */
98
+ readonly action?: string;
99
+ /**
100
+ * Where a completed-but-unenriched bind left its config, set only with
101
+ * `action: 'bind_registered_load_config'`.
102
+ *
103
+ * Absent from `message` and from {@link MeteorCloudError.toSafeObject} on
104
+ * purpose: an absolute path carries the local account name, and both of those
105
+ * are written to logs and evidence files. Read it off the error.
106
+ */
107
+ readonly configPath?: string;
108
+ /** A log-safe projection: no `cause`, no local paths, no credential material. */
109
+ toSafeObject(): SafeError;
110
+ }
111
+
112
+ // --- key storage -----------------------------------------------------------
113
+
114
+ /**
115
+ * The private key never leaves the store: `sign` takes bytes and returns a
116
+ * signature. A future non-exportable CNG backend satisfies this interface
117
+ * unchanged.
118
+ */
119
+ export declare abstract class KeyStore {
120
+ readonly scheme: string;
121
+ /** Creates a new P-256 key. Must refuse to overwrite an existing one. */
122
+ create(name: string): Promise<string>;
123
+ publicJwk(keyReference: string): Promise<JsonWebKey>;
124
+ /** @returns the 64-byte IEEE P1363 (JOSE) ECDSA signature over `data`. */
125
+ sign(keyReference: string, data: Buffer): Promise<Buffer>;
126
+ exists(keyReference: string): Promise<boolean>;
127
+ destroy(keyReference: string): Promise<void>;
128
+ }
129
+
130
+ /** Tier 2 — PKCS#8 PEM at `file:///<abs path>`, mode 0600. Dev / non-Windows. */
131
+ export declare class FileKeyStore extends KeyStore {
132
+ readonly scheme: 'file';
133
+ }
134
+
135
+ /**
136
+ * The optional native binding `DpapiKeyStore` requires. Ship it as
137
+ * `@meteorlive/dpapi`; it must be a thin CryptProtectData / CryptUnprotectData
138
+ * wrapper scoped to `CurrentUser`.
139
+ */
140
+ export interface DpapiBackend {
141
+ protectData(plaintext: Buffer, entropy?: Buffer): Buffer;
142
+ unprotectData(ciphertext: Buffer, entropy?: Buffer): Buffer;
143
+ }
144
+
145
+ /** Tier 1 (Windows) — DPAPI-wrapped PKCS#8 DER at `dpapi:///<abs path>`. */
146
+ export declare class DpapiKeyStore extends KeyStore {
147
+ constructor(options?: { backend?: DpapiBackend; entropy?: Buffer });
148
+ readonly scheme: 'dpapi';
149
+ }
150
+
151
+ /**
152
+ * What `CngKeyStore` needs from `@meteorlive/cng`. Note what is absent: there
153
+ * is no way to obtain the private key, because there is no such API — the key
154
+ * is created non-exportable inside the KSP and signing happens there.
155
+ */
156
+ export interface CngBackend {
157
+ /** Creates a non-exportable P-256 key. Throws rather than overwrite. */
158
+ createKey(container: string): void;
159
+ /** BCRYPT_ECCKEY_BLOB: magic, cbKey, X, Y. */
160
+ publicKeyBlob(container: string): Buffer;
161
+ /** @returns fixed-width r||s — already JOSE P1363, no DER to convert. */
162
+ signDigest(container: string, digest: Buffer): Buffer;
163
+ keyExists(container: string): boolean;
164
+ deleteKey(container: string): boolean;
165
+ }
166
+
167
+ /**
168
+ * Tier 0 (Windows, strongest) — a non-exportable CNG key at
169
+ * `cng://meteorlive/<id>`.
170
+ *
171
+ * Unlike every other tier the private key is never in this process: DPAPI stops
172
+ * it being readable on disk but hands the bytes back to sign, while here it is
173
+ * generated inside the key storage provider with the export policy cleared
174
+ * before finalize and never leaves. An attacker who owns the process can still
175
+ * ask the provider to sign while they hold the user's token; they cannot take
176
+ * the key and sign later, elsewhere, forever.
177
+ */
178
+ export declare class CngKeyStore extends KeyStore {
179
+ constructor(options?: { backend?: CngBackend });
180
+ readonly scheme: 'cng';
181
+ }
182
+
183
+ export declare function createKeyStore(
184
+ keyReference: string,
185
+ options?: { dpapi?: { backend?: DpapiBackend; entropy?: Buffer }; cng?: { backend?: CngBackend } }
186
+ ): KeyStore;
187
+
188
+ /**
189
+ * `path` for the file-backed tiers; `container` for `cng://`, which never
190
+ * touches the filesystem. Exactly one is present.
191
+ */
192
+ export declare function parseKeyReference(keyReference: string): {
193
+ scheme: KeyReferenceScheme;
194
+ path?: string;
195
+ container?: string;
196
+ };
197
+
198
+ /** `'dpapi'` on win32, `'file'` elsewhere. Pass `keyStoreScheme` to override. */
199
+ export declare function defaultKeyStoreScheme(): 'file' | 'dpapi';
200
+
201
+ // --- installation config ---------------------------------------------------
202
+
203
+ /**
204
+ * The complete on-disk config. Every member is non-secret by construction, and
205
+ * the list is closed: there is no camera, no station, no coordinate and no
206
+ * key-to-uid map here, because the SDK holds no inventory (contract §1.2).
207
+ */
208
+ export interface InstallationConfig {
209
+ schema: 'mlc.installation/1';
210
+ issuer: string;
211
+ clientId: ProductClientId;
212
+ installationUid: string;
213
+ keyReference: string;
214
+ }
215
+
216
+ export declare function loadInstallationConfig(configPath: string): Promise<InstallationConfig>;
217
+
218
+ export declare function saveInstallationConfig(
219
+ configPath: string,
220
+ config: {
221
+ issuer: string;
222
+ client_id?: ProductClientId;
223
+ clientId?: ProductClientId;
224
+ installation_uid?: string;
225
+ installationUid?: string;
226
+ key_reference?: string;
227
+ keyReference?: string;
228
+ }
229
+ ): Promise<InstallationConfig>;
230
+
231
+ // --- account, stations, cameras --------------------------------------------
232
+
233
+ /**
234
+ * The authorized account, minimum fields only (contract §7.2, §10.4): no phone
235
+ * number, no department, no role list.
236
+ */
237
+ export interface AccountSummary {
238
+ accountId: string;
239
+ nickname: string;
240
+ tenantId?: string;
241
+ }
242
+
243
+ /**
244
+ * One station registered under this account (contract §6.5).
245
+ *
246
+ * A station is a *position*, not a device: two cameras that declare the same
247
+ * coordinates land on the same station, and a camera that moves is re-hung on
248
+ * another one while the old station stays (§7.9). The client never edits a
249
+ * station.
250
+ */
251
+ export interface StationSummary {
252
+ stationUid: string;
253
+ stationCode: string;
254
+ displayName: string;
255
+ /**
256
+ * Every station a client creates is `temporary`. `permanent` is set by a
257
+ * person on the web console, and nothing about the upload path changes when
258
+ * they do (plan §0.4).
259
+ */
260
+ kind: 'permanent' | 'temporary';
261
+ /** Reserved. Undefined until the science-quality track (S9-S12) ships. */
262
+ positionSigmaHorizontalM?: number;
263
+ /** Reserved. Undefined until the science-quality track (S9-S12) ships. */
264
+ positionSigmaVerticalM?: number;
265
+ /** Reserved. Undefined until the science-quality track (S9-S12) ships. */
266
+ validFromUtc?: string;
267
+ /** Reserved. Undefined until the science-quality track (S9-S12) ships. */
268
+ validToUtc?: string;
269
+ countryCode?: string;
270
+ provinceCode?: string;
271
+ cityName?: string;
272
+ districtName?: string;
273
+ /** Resolved IANA zone; absent/null means pending. */
274
+ timezone?: string | null;
275
+ /**
276
+ * `active`, `revoked` (set from the console — `MsCloudStationSaveReqVO`
277
+ * accepts `active|revoked`) or `archived` for a retired temporary station
278
+ * (`MsCloudStationKinds.STATUS_ARCHIVED`). `disabled` was never a server
279
+ * value. The union keeps `string` on purpose: the vocabulary is the server's
280
+ * to grow, and a closed union would turn every future state into a type
281
+ * error in host code that only wanted to display it.
282
+ */
283
+ status: 'active' | 'revoked' | 'archived' | string;
284
+ /**
285
+ * When an idle temporary station was retired, ISO-8601 UTC. Sent only for an
286
+ * archived station, so its presence is the signal.
287
+ *
288
+ * It is the only way to tell "my position stopped matching" apart from "my
289
+ * position was never registered": an archived station keeps its row but stops
290
+ * matching by position, so the next upload from the same coordinates quietly
291
+ * gets a fresh station (contract §7.9).
292
+ *
293
+ * No server writes it yet — nothing sets a station to `archived`, so in
294
+ * practice this is always undefined today.
295
+ */
296
+ archivedAt?: string;
297
+ [key: string]: unknown;
298
+ }
299
+
300
+ /** One camera registered under this account (contract §6.5). */
301
+ export interface CameraSummary {
302
+ cameraUid: string;
303
+ cameraCode: string;
304
+ /** The host application's own stable key. Present only on cameras a `camera` block registered. */
305
+ cameraKey?: string;
306
+ displayName: string;
307
+ stationUid: string;
308
+ stationCode: string;
309
+ /**
310
+ * `active` or `revoked` — the two values `MsCloudCameraSaveReqVO`'s
311
+ * `@Pattern` accepts. There is no `disabled` on the camera axis either, and a
312
+ * camera is never `archived`: only stations are. `string` is kept for the
313
+ * same reason as on {@link StationSummary.status}.
314
+ */
315
+ status: 'active' | 'revoked' | string;
316
+ configVersion?: number;
317
+ [key: string]: unknown;
318
+ }
319
+
320
+ /**
321
+ * The station half of a machine declaration (contract §7.9).
322
+ *
323
+ * The coordinates and elevation come from the host application's own site configuration —
324
+ * a meteor detector already knows them, because astrometry needs them. They are
325
+ * what identifies the station: same rounded coordinates under one account means
326
+ * the same station.
327
+ */
328
+ export interface StationBlock {
329
+ /** WGS84 degrees, -90 to 90. */
330
+ latitude: number;
331
+ /** WGS84 degrees, -180 to 180. */
332
+ longitude: number;
333
+ /** Whole metres, -500 to 10000. */
334
+ elevationM: number;
335
+ /** Optional IANA override; omitted means server coordinate lookup. UTC offsets are refused. */
336
+ timezone?: string;
337
+ /** Adopted when the station is first created; ignored afterwards. */
338
+ name?: string;
339
+ }
340
+
341
+ /**
342
+ * A machine declaration: which channel this is, and where it observes from.
343
+ *
344
+ * Carried on every upload. The server registers on first sight and answers with
345
+ * the uids purely as information — the host application does not have to store
346
+ * them, and the SDK stores nothing.
347
+ */
348
+ export interface CameraBlock {
349
+ /**
350
+ * The host application's own stable key for this channel: 1-64 characters of
351
+ * `A-Za-z0-9._:-` starting with a letter or digit, carrying no PII.
352
+ * Generate it once with {@link newCameraKey} and persist it in your own
353
+ * configuration. Changing it means a different camera.
354
+ */
355
+ key: string;
356
+ /** Display name. Adopted on first sight; a later change renames the camera. */
357
+ name?: string;
358
+ station: StationBlock;
359
+ }
360
+
361
+ /** What the server answers about a declaration. Informational — nothing needs storing. */
362
+ export interface CameraRegistration {
363
+ cameraUid: string;
364
+ stationUid: string;
365
+ createdCamera?: boolean;
366
+ createdStation?: boolean;
367
+ [key: string]: unknown;
368
+ }
369
+
370
+ /**
371
+ * One page of a list endpoint. Not a snapshot and never cached: the account can
372
+ * gain, lose or disable a camera on the web console at any moment, and the next
373
+ * upload is judged on the server's live facts (contract §6.6).
374
+ */
375
+ export interface Page<T> {
376
+ list: T[];
377
+ total?: number;
378
+ [key: string]: unknown;
379
+ }
380
+
381
+ /** Pagination and filters only. The server derives owner and tenant from the token. */
382
+ export interface StationQuery {
383
+ pageNo?: number;
384
+ pageSize?: number;
385
+ status?: string;
386
+ }
387
+
388
+ /** Pagination and filters only. The server derives owner and tenant from the token. */
389
+ export interface CameraQuery {
390
+ pageNo?: number;
391
+ pageSize?: number;
392
+ stationUid?: string;
393
+ status?: string;
394
+ }
395
+
396
+ /**
397
+ * A random 32-hex stable key for one host-app channel. Pure local: no network,
398
+ * no state, no PII. Call it once per channel and write the result into your own
399
+ * configuration — never on each start, and never derived from a hostname or MAC.
400
+ */
401
+ export declare function newCameraKey(): string;
402
+
403
+ // --- first bind ------------------------------------------------------------
404
+
405
+ export interface ConnectOptions {
406
+ /** Must carry the `/cloud` path segment; it is the issuer identity, not a hostname. */
407
+ issuer: string;
408
+ clientId: ProductClientId;
409
+ /** Where the zero-secret config is written. */
410
+ configPath: string;
411
+ /** Path for a new key. Required unless `keyReference` names an existing one. */
412
+ keyPath?: string;
413
+ keyReference?: string;
414
+ keyStore?: KeyStore;
415
+ keyStoreScheme?: KeyReferenceScheme;
416
+ dpapi?: { backend?: DpapiBackend; entropy?: Buffer };
417
+ cng?: { backend?: CngBackend };
418
+ /**
419
+ * `'system'` (default) opens the desktop's default browser. `'manual'` opens
420
+ * nothing: take the URL from {@link PendingConnect.authorizationUrl}, or pass
421
+ * `onAuthorizeUrl` — `connect()` refuses manual mode without one, because
422
+ * otherwise the URL never reaches anybody and the call just times out.
423
+ */
424
+ browserMode?: 'system' | 'manual';
425
+ /** Replace how the URL reaches a browser. Cannot be combined with `browserMode: 'manual'`. */
426
+ openBrowser?: (url: string) => void | Promise<void>;
427
+ /**
428
+ * Receives the authorization URL before the browser is opened.
429
+ *
430
+ * The URL no longer carries a session credential — `bridge` is gone — but it
431
+ * still carries `state` and the loopback port, and contract §10.1 forbids
432
+ * logging it. Hand it to a browser; do not print it.
433
+ */
434
+ onAuthorizeUrl?: (url: string) => void | Promise<void>;
435
+ /** Aborting settles the flow with `kind: 'cancelled'`, `action: 'authorize_aborted'`. */
436
+ signal?: AbortSignal;
437
+ /** How long to wait for the browser callback. Default 300000. */
438
+ timeoutMs?: number;
439
+ httpTimeoutMs?: number;
440
+ /** Options for the {@link MeteorCloudClient} built from the result. */
441
+ clientOptions?: ClientOptions;
442
+ transport?: Transport;
443
+ randomBytes?: (size: number) => Buffer | Uint8Array;
444
+
445
+ /** @deprecated Removed in 0.5.0: the browser signs in on its own and the SDK never relays a session grant. */
446
+ bridgeGrant?: never;
447
+ /** @deprecated Removed in 0.5.0: the consent page shows no cameras; declare the camera on each upload instead. */
448
+ proposal?: never;
449
+ /** @deprecated Removed in 0.5.0: the consent page submits no per-camera decisions. */
450
+ installationDecisions?: never;
451
+ /** @deprecated Removed in 0.5.0: the SDK neither reads nor forwards host capture channels. */
452
+ channels?: never;
453
+ /** @deprecated Removed in 0.5.0: the SDK neither reads nor forwards host devices. */
454
+ devices?: never;
455
+ /** @deprecated Removed in 0.5.0: the SDK holds no camera inventory. */
456
+ cameras?: never;
457
+ /** @deprecated Removed in 0.5.0: the grant is account-wide; name the camera on each upload instead. */
458
+ cameraUid?: never;
459
+ /** @deprecated Removed in 0.5.0: the grant is account-wide; the station follows from the upload. */
460
+ stationUid?: never;
461
+ }
462
+
463
+ export interface InstallationSummary {
464
+ installationUid: string;
465
+ productClientId: ProductClientId;
466
+ status: 'active';
467
+ }
468
+
469
+ export interface ConnectResult {
470
+ /** Ready to use. Everything below is a convenience the bind already paid for. */
471
+ client: MeteorCloudClient;
472
+ /**
473
+ * The first operational token (contract §2.2, §6.1). Interoperability only —
474
+ * it is **not** a signal that you should manage renewal yourself; the client
475
+ * mints and renews on demand. Memory only: never write it to
476
+ * `installation.json`, a journal, a log or a crash report.
477
+ */
478
+ token: AccessToken;
479
+ account: AccountSummary;
480
+ installation: InstallationSummary;
481
+ config: InstallationConfig;
482
+ configPath: string;
483
+ }
484
+
485
+ /**
486
+ * A bind in progress. The loopback listener is already up when you receive
487
+ * this, so the URL is safe to open immediately — a consent redirect that lands
488
+ * on a port which has not been bound yet is unrecoverable.
489
+ */
490
+ export interface PendingConnect {
491
+ authorizationUrl: string;
492
+ wait(): Promise<ConnectResult>;
493
+ /** Settles `wait()` with `kind: 'cancelled'`; reports nothing itself. */
494
+ cancel(): Promise<void>;
495
+ }
496
+
497
+ /**
498
+ * The account namespace (contract §6.1). `MeteorCloud.connect()` is the one
499
+ * call a first-time integration needs.
500
+ */
501
+ export declare const MeteorCloud: {
502
+ /**
503
+ * First bind, one call: generate a local P-256 key, open the browser at the
504
+ * authorization endpoint (code + PKCE S256, loopback redirect on an
505
+ * OS-assigned port), wait for the callback, exchange the code, persist a
506
+ * zero-secret config, mint the first operational token and read the account.
507
+ *
508
+ * The user signs in with the ordinary MeteorCloud web login (including
509
+ * WeChat) and clicks one "allow" screen. No grant to copy, no code to paste,
510
+ * and no camera to pick.
511
+ */
512
+ connect(options: ConnectOptions): Promise<ConnectResult>;
513
+ /** Two-phase bind for callers that open the URL themselves (Electron, an embedded WebView). */
514
+ beginConnect(options: ConnectOptions): Promise<PendingConnect>;
515
+ /** A random stable key for one host-app channel. Pure local. */
516
+ newCameraKey(): string;
517
+ };
518
+
519
+ /** @deprecated Prefer {@link MeteorCloud.connect}. Same function, kept for the migration window. */
520
+ export declare function connect(options: ConnectOptions): Promise<ConnectResult>;
521
+
522
+ /** Two-phase bind. See {@link MeteorCloud.beginConnect}. */
523
+ export declare function beginConnect(options: ConnectOptions): Promise<PendingConnect>;
524
+
525
+ // --- transport -------------------------------------------------------------
526
+
527
+ export interface TransportRequest {
528
+ method: string;
529
+ url: string;
530
+ headers: Record<string, string>;
531
+ body?: Buffer;
532
+ /** A *factory* — a retried chunk needs a fresh stream over the same bytes. */
533
+ bodyStream?: () => NodeJS.ReadableStream;
534
+ timeoutMs: number;
535
+ maxResponseBytes?: number;
536
+ }
537
+
538
+ export interface TransportResponse {
539
+ status: number;
540
+ headers: Headers | Record<string, string>;
541
+ body: Buffer;
542
+ }
543
+
544
+ export type Transport = (request: TransportRequest) => Promise<TransportResponse>;
545
+
546
+ // --- tokens ----------------------------------------------------------------
547
+
548
+ export interface AccessToken {
549
+ accessToken: string;
550
+ tokenType: string;
551
+ scope: string;
552
+ /** Absolute epoch seconds. */
553
+ expiresAt: number;
554
+ /** Truncated SHA-256 of the token, safe to log. */
555
+ fingerprint: string;
556
+ }
557
+
558
+ export interface InstallationTokenSourceOptions {
559
+ issuer: string;
560
+ installationUid: string;
561
+ keyStore: KeyStore;
562
+ keyReference: string;
563
+ scopes?: string[];
564
+ /** Send a DPoP proof with every request. Default true. */
565
+ dpop?: boolean;
566
+ /** Default `'auto'`: DPoP when the token is DPoP-bound, else Bearer. */
567
+ authScheme?: 'auto' | 'bearer' | 'dpop';
568
+ transport?: Transport;
569
+ timeoutMs?: number;
570
+ attempts?: number;
571
+ /** Renew this many seconds before nominal expiry. Default 60. */
572
+ marginSeconds?: number;
573
+ now?: () => number;
574
+ random?: () => number;
575
+ sleep?: (ms: number) => Promise<void>;
576
+ jti?: () => string;
577
+ }
578
+
579
+ /** Mints short-lived opaque device tokens from the local key (RFC 7523). */
580
+ export declare class InstallationTokenSource {
581
+ constructor(options: InstallationTokenSourceOptions);
582
+ getToken(): Promise<AccessToken>;
583
+ /** Drops the cached token so the next call re-asserts. */
584
+ invalidate(): void;
585
+ noteResourceNonce(nonce?: string): void;
586
+ publicJwk(): Promise<JsonWebKey>;
587
+ authorizationHeaders(method: string, url: string): Promise<Record<string, string>>;
588
+ /** RFC 7523 client assertion, ES256, `exp - iat = 60`. */
589
+ createAssertion(): Promise<string>;
590
+ /** RFC 9449 DPoP proof. `ath` is included only when `accessToken` is given. */
591
+ createProof(
592
+ method: string,
593
+ url: string,
594
+ options?: { accessToken?: string; nonce?: string }
595
+ ): Promise<string>;
596
+ }
597
+
598
+ // --- journal ---------------------------------------------------------------
599
+
600
+ export interface JournalFileState {
601
+ /** Relative upload target path; the local plane has no COS object key. */
602
+ key: string;
603
+ size: number;
604
+ sha256: string;
605
+ /** Last offset the client believes the server holds. The server is authoritative. */
606
+ offset: number;
607
+ }
608
+
609
+ export interface JournalState {
610
+ version: 2;
611
+ sessionUid: string;
612
+ files: Record<string, JournalFileState>;
613
+ }
614
+
615
+ export declare class UploadJournal {
616
+ constructor(journalPath: string, sessionUid: string);
617
+ readonly path: string;
618
+ readonly sessionUid: string;
619
+ state: JournalState;
620
+ load(): Promise<this>;
621
+ fileState(role: ArtifactRole | string): JournalFileState;
622
+ reconcile(
623
+ role: ArtifactRole | string,
624
+ identity: { key: string; size: number; sha256: string }
625
+ ): JournalFileState;
626
+ save(): Promise<void>;
627
+ /**
628
+ * The last `io` failure swallowed by a best-effort journal write, or
629
+ * undefined. The journal is advisory — a full disk must not abort an upload
630
+ * that needs no local disk — so failures are recorded here instead of thrown.
631
+ * Read it if you want to warn about a station whose disk is failing.
632
+ */
633
+ lastSaveError?: MeteorCloudError;
634
+ }
635
+
636
+ // --- client ----------------------------------------------------------------
637
+
638
+ export interface ClientOptions {
639
+ /** Defaults to the issuer origin. */
640
+ apiBase?: string;
641
+ httpTimeoutMs?: number;
642
+ /** Timeout for one chunk PUT. Default 600000. */
643
+ chunkTimeoutMs?: number;
644
+ controlAttempts?: number;
645
+ chunkAttempts?: number;
646
+ /** Default and maximum 32 MiB. Clamped to `capabilities.directPut.maxChunkBytes`. */
647
+ chunkSize?: number;
648
+ dpop?: boolean;
649
+ authScheme?: 'auto' | 'bearer' | 'dpop';
650
+ tokenMarginSeconds?: number;
651
+ keyStore?: KeyStore;
652
+ /** Only consulted when `keyStore` is absent and the reference is `dpapi://`. */
653
+ dpapi?: { backend?: DpapiBackend; entropy?: Buffer };
654
+ /** Only consulted when `keyStore` is absent and the reference is `cng://`. */
655
+ cng?: { backend?: CngBackend };
656
+ tokenSource?: InstallationTokenSource;
657
+ transport?: Transport;
658
+ now?: () => number;
659
+ random?: () => number;
660
+ sleep?: (ms: number) => Promise<void>;
661
+ jti?: () => string;
662
+ }
663
+
664
+ export interface ClientConfig extends ClientOptions {
665
+ issuer: string;
666
+ clientId: ProductClientId;
667
+ installationUid: string;
668
+ keyReference: string;
669
+ }
670
+
671
+ interface AuthorizeRequestBase {
672
+ clientRequestKey: string;
673
+ manifestJson: string;
674
+ artifacts: Artifact[];
675
+ onProgress?: ProgressCallback;
676
+ }
677
+
678
+ /**
679
+ * Where an upload came from. Exactly one of the two is required, and both are
680
+ * accepted together (contract §7.6, §7.9).
681
+ *
682
+ * - `camera` is the supported shape: the host application's own stable key plus
683
+ * the station's coordinates, replayed on every upload. The server registers
684
+ * on first sight and answers with the uids as information.
685
+ * - `cameraUid` names a camera you already happen to know — from a previous
686
+ * `ensureCamera()`, or from `listCameras()`. It is authoritative when both
687
+ * are given, and the server checks the registered key against `camera.key`.
688
+ *
689
+ * Neither one is a *grant*: the server resolves ownership from the token every
690
+ * time, so this selects a camera and never confers access to one.
691
+ */
692
+ export type AuthorizeRequest =
693
+ | (AuthorizeRequestBase & { camera: CameraBlock; cameraUid?: string })
694
+ | (AuthorizeRequestBase & { cameraUid: string; camera?: CameraBlock });
695
+
696
+ /** One event: an {@link AuthorizeRequest} plus the journal that makes it resumable. */
697
+ export type UploadEventRequest = AuthorizeRequest & { journalPath: string };
698
+
699
+ /** Per-artifact PUT target. Present only for `local_direct` sessions. */
700
+ export interface UploadTarget {
701
+ role: ArtifactRole;
702
+ /** Relative Device API path, e.g. `upload-sessions/us_.../artifacts/ecsv`. */
703
+ path: string;
704
+ /** The server's durable count — the only legal offset for the next chunk. */
705
+ receivedBytes: number;
706
+ sizeBytes: number;
707
+ }
708
+
709
+ export interface UploadSession {
710
+ sessionUid: string;
711
+ uploadUid?: string;
712
+ jobUid?: string;
713
+ status: string;
714
+ duplicate?: boolean;
715
+ expiresAt?: string;
716
+ /**
717
+ * Where the declaration landed. Echoed on the `authorize` response only —
718
+ * that is the hop that registers the machine — so `uploadEvent()`, which
719
+ * resolves with the *finalize* response, does not carry them. Informational
720
+ * either way; nothing needs storing.
721
+ */
722
+ stationUid?: string;
723
+ cameraUid?: string;
724
+ createdStation?: boolean;
725
+ createdCamera?: boolean;
726
+ uploadTargets?: UploadTarget[];
727
+ files?: Record<string, unknown>[];
728
+ capabilities?: Record<string, unknown>;
729
+ [key: string]: unknown;
730
+ }
731
+
732
+ export interface JobQueryCapability {
733
+ observed: boolean;
734
+ supported: boolean;
735
+ compatible: boolean;
736
+ contractVersion?: string;
737
+ minimumPollSeconds: number;
738
+ }
739
+
740
+ export interface JobStatus {
741
+ contractVersion: 'mlc.device-job-status/2' | string;
742
+ uploadUid: string;
743
+ jobUid: string;
744
+ submissionStatus: string;
745
+ jobStatus: 'queued' | 'running' | 'succeeded' | 'failed' | 'dead' | string;
746
+ resultStatus: string;
747
+ resultFinal: boolean;
748
+ terminal: boolean;
749
+ nextPollAfterSeconds?: number | null;
750
+ reasonCode?: string | null;
751
+ reasonMessage?: string | null;
752
+ attemptCount?: number;
753
+ requeueCount?: number;
754
+ updatedAt: string;
755
+ result?: Record<string, unknown> | null;
756
+ [key: string]: unknown;
757
+ }
758
+
759
+ export declare class MeteorCloudClient {
760
+ constructor(config: ClientConfig);
761
+
762
+ readonly issuer: string;
763
+ readonly apiBase: string;
764
+ readonly clientId: ProductClientId;
765
+ readonly installationUid: string;
766
+ readonly keyReference: string;
767
+ readonly keyStore: KeyStore;
768
+ readonly tokens: InstallationTokenSource;
769
+
770
+ static fromConfig(configPath: string, options?: ClientOptions): Promise<MeteorCloudClient>;
771
+
772
+ /**
773
+ * @deprecated Removed. The bind entry points are {@link MeteorCloud.connect}
774
+ * and {@link MeteorCloud.beginConnect} (plan §0.9), and nothing on the class
775
+ * duplicates them. Declared as `never` so a caller still on the 0.4.x
776
+ * spelling gets a compile error naming the replacement rather than a runtime
777
+ * "is not a function".
778
+ */
779
+ static connect?: never;
780
+
781
+ /**
782
+ * Whether this client has a complete identity AND a usable private key.
783
+ *
784
+ * `fromConfig()` never touches the key, so a config that outlived its key
785
+ * loads fine and fails at the first token mint — after `uploadEvent()` has
786
+ * already hashed every artifact. Check this at startup instead. Signs a probe
787
+ * rather than stat-ing, because a DPAPI blob can exist and still be
788
+ * unreadable by the current Windows user.
789
+ */
790
+ isBound(): Promise<boolean>;
791
+
792
+ /**
793
+ * The current operational token, minted or renewed as needed.
794
+ *
795
+ * Interoperability only. Ordinary integrations never call this: the client
796
+ * already attaches `Authorization` and the DPoP proof to every request, and a
797
+ * token you hold yourself is one you have to keep out of your own logs.
798
+ */
799
+ getAccessToken(): Promise<AccessToken>;
800
+
801
+ /** The authorized account (contract §7.2). Requires `account:read`. */
802
+ getAccount(): Promise<AccountSummary>;
803
+
804
+ /**
805
+ * The stations already registered under this account (contract §7.3).
806
+ *
807
+ * An ordinary query, not a snapshot and not cached. Call it when your UI
808
+ * needs the list — never on connect, renewal or upload. The server narrows
809
+ * to the token's owner and tenant, so the query carries pagination and
810
+ * filters only; an owner-shaped key is refused locally.
811
+ */
812
+ listStations(query?: StationQuery): Promise<Page<StationSummary>>;
813
+
814
+ /**
815
+ * The cameras already registered under this account (contract §7.4).
816
+ *
817
+ * This queries MeteorCloud; it does not scan the machine. A host application
818
+ * that carries a `camera` block on each upload never needs it.
819
+ */
820
+ listCameras(query?: CameraQuery): Promise<Page<CameraSummary>>;
821
+
822
+
823
+ /**
824
+ * One camera by uid. A camera that does not exist and one that belongs to
825
+ * somebody else are deliberately indistinguishable (both 404, contract §9.4).
826
+ */
827
+ getCamera(cameraUid: string): Promise<CameraSummary>;
828
+
829
+ /**
830
+ * Optional pre-registration (contract §7.9). The argument is the camera block
831
+ * itself — `{key, name?, station}`, not `{camera: {...}}` — and the upsert is
832
+ * the same one an upload performs.
833
+ *
834
+ * Optional means optional: carrying the block on each `uploadEvent()` is the
835
+ * supported path, and even here the returned uid does not have to be stored.
836
+ */
837
+ ensureCamera(input: CameraBlock): Promise<CameraRegistration>;
838
+
839
+ /** Snapshot from the latest session response; malformed/absent capability disables polling. */
840
+ jobQueryCapability(): JobQueryCapability;
841
+ /** One HTTP resource request; schedule retries using nextPollAfterSeconds or the error. */
842
+ jobStatus(jobUid: string): Promise<JobStatus>;
843
+
844
+ authorize(request: AuthorizeRequest): Promise<UploadSession>;
845
+ uploadSessionStatus(sessionUid: string): Promise<UploadSession>;
846
+ refresh(sessionUid: string): Promise<UploadSession>;
847
+ /** Sends `{files: []}` — the local plane carries no receipts. */
848
+ finalize(sessionUid: string): Promise<UploadSession>;
849
+ abort(sessionUid: string, reason?: string): Promise<boolean>;
850
+
851
+ uploadAuthorized(
852
+ session: UploadSession,
853
+ artifacts: Artifact[],
854
+ journalPath: string,
855
+ onProgress?: ProgressCallback
856
+ ): Promise<JournalState>;
857
+
858
+ /** authorize → chunked PUT → finalize, resumable at every step. */
859
+ uploadEvent(request: UploadEventRequest): Promise<UploadSession>;
860
+ }
861
+
862
+ // --- low-level JOSE helpers ------------------------------------------------
863
+
864
+ export declare const jose: {
865
+ ES256: 'ES256';
866
+ P256_CRV: 'P-256';
867
+ base64UrlEncode(input: Buffer | Uint8Array | string): string;
868
+ base64UrlEncodeJson(value: unknown): string;
869
+ base64UrlDecode(value: string): Buffer;
870
+ randomToken(bytes?: number): string;
871
+ /** RFC 7636 S256. */
872
+ pkceChallengeS256(verifier: string): string;
873
+ createPkcePair(randomBytes?: (size: number) => Buffer | Uint8Array): {
874
+ verifier: string;
875
+ challenge: string;
876
+ method: 'S256';
877
+ };
878
+ p256PublicJwk(key: unknown): JsonWebKey;
879
+ /** RFC 7638 thumbprint, base64url SHA-256 — matches the server's `cnf.jkt`. */
880
+ jwkThumbprint(jwk: JsonWebKey): string;
881
+ signCompactJws(
882
+ header: Record<string, unknown>,
883
+ claims: Record<string, unknown>,
884
+ sign: (signingInput: Buffer) => Promise<Buffer> | Buffer
885
+ ): Promise<string>;
886
+ /** RFC 9449 `htu`: the request URI without query or fragment. */
887
+ dpopHtu(url: string): string;
888
+ };
889
+
890
+ export declare const SDK_VERSION: '0.5.1';
891
+ export declare const CONFIG_SCHEMA: 'mlc.installation/1';
892
+ export declare const DIRECT_PUT_CONTRACT: 'mlc.device-direct-put/1';
893
+ export declare const JOURNAL_VERSION: 2;
894
+ /**
895
+ * The scope group an operational token requests (contract §7.1, plan §1.2).
896
+ *
897
+ * Defined and checked per endpoint from day one; v0.5.0 still issues the whole
898
+ * group, so narrowing a grant later needs no second breaking change.
899
+ * `device-context:read` is not in the group and is not accepted as an alias:
900
+ * `/device-context` was deleted outright along with the bound-camera model it
901
+ * reported (plan §0.9), so there is nothing for the old name to stand for.
902
+ */
903
+ export declare const DEVICE_SCOPES: readonly [
904
+ 'account:read',
905
+ 'installation:read',
906
+ 'station:read',
907
+ 'camera:read',
908
+ 'camera:write',
909
+ 'ingest:create',
910
+ 'ingest:finalize',
911
+ 'job:read'
912
+ ];
913
+ export declare const BIND_SCOPE: 'mscloud.bind';
914
+ export declare const CLIENT_ASSERTION_TYPE: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
915
+ export declare const DPAPI_MODULE: '@meteorlive/dpapi';
916
+ export declare const CNG_MODULE: '@meteorlive/cng';
917
+ export declare const ROLES: readonly ArtifactRole[];
918
+ export declare const DEFAULT_CHUNK_SIZE: number;
919
+ export declare const MAX_CHUNK_SIZE: number;
920
+ /** Device API business codes the data plane can return. */
921
+ export declare const API_CODE: {
922
+ readonly DIGEST_MISMATCH: 1030002005;
923
+ readonly REQUEST_TOO_LARGE: 1030003010;
924
+ readonly CHUNK_OUT_OF_ORDER: 1030003030;
925
+ readonly CHUNK_RANGE_INVALID: 1030003031;
926
+ readonly ARTIFACT_ALREADY_COMPLETE: 1030003032;
927
+ readonly DIRECT_PUT_DISABLED: 1030003034;
928
+ /** authorize declared no camera: neither a `camera` block nor a `cameraUid`. */
929
+ readonly CAMERA_REQUIRED: 1030003035;
930
+ };