stitchkit 0.76.1 → 0.77.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/agent-runtime-coding-tools.js +1 -2
  2. package/dist/agent-runtime-harness.js +4 -5
  3. package/dist/agent-runtime.js +7 -8
  4. package/dist/application/decisions.d.ts +86 -0
  5. package/dist/application/decisions.d.ts.map +1 -0
  6. package/dist/application/kernel.d.ts +33 -1
  7. package/dist/application/kernel.d.ts.map +1 -1
  8. package/dist/application/watch-hub.d.ts +1 -1
  9. package/dist/application/watch-hub.d.ts.map +1 -1
  10. package/dist/application-schemas.d.ts +15 -0
  11. package/dist/application-schemas.d.ts.map +1 -0
  12. package/dist/application-schemas.js +111 -0
  13. package/dist/application.d.ts +3 -2
  14. package/dist/application.d.ts.map +1 -1
  15. package/dist/application.js +184 -114
  16. package/dist/cli.js +6 -7
  17. package/dist/contract/index.js +1 -2
  18. package/dist/{index-v5dm4bwq.js → index-58jzmnn4.js} +1 -1
  19. package/dist/{index-885chjxq.js → index-7qy2ex0m.js} +1 -1
  20. package/dist/index-a916km3r.js +45 -0
  21. package/dist/{index-28jsdwt1.js → index-aczggrty.js} +1 -1
  22. package/dist/{index-65kathdm.js → index-da1aqnhb.js} +4 -6
  23. package/dist/{index-xgahrwdf.js → index-ezmn6ac6.js} +3 -3
  24. package/dist/{index-4kp4fxvp.js → index-f6n5n7nz.js} +18 -4
  25. package/dist/{index-6wr9f6qm.js → index-jqtsc9mj.js} +2 -2
  26. package/dist/{index-9c5dbr5q.js → index-k2zczx1g.js} +1 -1
  27. package/dist/{index-v7xjfy10.js → index-m668wzyc.js} +1 -1
  28. package/dist/{index-e0a57ymn.js → index-nemjkxjp.js} +1 -1
  29. package/dist/{index-9my2n66s.js → index-s2rchahr.js} +4 -4
  30. package/dist/{index-k68e5jz0.js → index-s4c8wy8m.js} +2 -2
  31. package/dist/{index-52400rdd.js → index-t8qyvrvg.js} +3 -3
  32. package/dist/{index-endzd8sj.js → index-vbf2p6me.js} +1 -1
  33. package/dist/{index-hvftzz91.js → index-vc1b0b1b.js} +221 -117
  34. package/dist/{index-hsz2mmk0.js → index-vj3vvpaa.js} +17 -1
  35. package/dist/index.js +1 -2
  36. package/dist/internal/decision.d.ts +31 -0
  37. package/dist/internal/decision.d.ts.map +1 -0
  38. package/dist/internal/stable-digest.d.ts +40 -6
  39. package/dist/internal/stable-digest.d.ts.map +1 -1
  40. package/dist/live/events.d.ts +8 -13
  41. package/dist/live/events.d.ts.map +1 -1
  42. package/dist/live/watch-client.d.ts.map +1 -1
  43. package/dist/live.d.ts +1 -1
  44. package/dist/live.d.ts.map +1 -1
  45. package/dist/live.js +40 -57
  46. package/dist/node.js +4 -5
  47. package/dist/observability/index.js +3 -4
  48. package/dist/primitives.js +1 -2
  49. package/dist/remote.js +2 -3
  50. package/dist/server/event-bus.d.ts +3 -2
  51. package/dist/server/event-bus.d.ts.map +1 -1
  52. package/dist/server/index.js +5 -6
  53. package/dist/testing.js +3 -4
  54. package/dist/tool-invoker.js +5 -6
  55. package/dist/tools.js +13 -14
  56. package/llms-full.txt +184 -4
  57. package/package.json +6 -2
  58. package/dist/index-rxfy4cq7.js +0 -17
  59. package/dist/index-vdvmcy7r.js +0 -22
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  collectTools,
3
3
  createToolRunner
4
- } from "./index-885chjxq.js";
4
+ } from "./index-7qy2ex0m.js";
5
5
  import {
6
6
  toolErrorFromResult
7
- } from "./index-k68e5jz0.js";
7
+ } from "./index-s4c8wy8m.js";
8
8
  import {
9
9
  assertUniqueToolName
10
10
  } from "./index-22by16v6.js";
@@ -3,7 +3,7 @@ import {
3
3
  } from "./index-wwst0td5.js";
4
4
  import {
5
5
  contractToolMountable
6
- } from "./index-885chjxq.js";
6
+ } from "./index-7qy2ex0m.js";
7
7
  import {
8
8
  projectToolSurface
9
9
  } from "./index-22by16v6.js";
@@ -15,7 +15,7 @@ import {
15
15
  import {
16
16
  ToolExecutionControlError,
17
17
  isToolExecutionControlError
18
- } from "./index-k68e5jz0.js";
18
+ } from "./index-s4c8wy8m.js";
19
19
  import {
20
20
  AgentAssistantPlaceholderSchema,
21
21
  AgentJsonObjectSchema,
@@ -3,7 +3,7 @@ import {
3
3
  } from "./index-nt1mp8km.js";
4
4
  import {
5
5
  isUnsafeKey
6
- } from "./index-hsz2mmk0.js";
6
+ } from "./index-vj3vvpaa.js";
7
7
  import {
8
8
  isRecord
9
9
  } from "./index-qyrqwr4c.js";
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  collectToolSurface
3
- } from "./index-9c5dbr5q.js";
3
+ } from "./index-k2zczx1g.js";
4
4
  import {
5
5
  createToolRunner,
6
6
  formatToolError
7
- } from "./index-885chjxq.js";
7
+ } from "./index-7qy2ex0m.js";
8
8
  import {
9
9
  coerceJsonArgs,
10
10
  toolResultFromError
11
- } from "./index-k68e5jz0.js";
11
+ } from "./index-s4c8wy8m.js";
12
12
  import {
13
13
  isWithinDir
14
14
  } from "./index-sbdmyz75.js";
@@ -27,7 +27,7 @@ import {
27
27
  import {
28
28
  isUnsafeKey,
29
29
  safeJsonParse
30
- } from "./index-hsz2mmk0.js";
30
+ } from "./index-vj3vvpaa.js";
31
31
  import {
32
32
  isRecord
33
33
  } from "./index-qyrqwr4c.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getRequestContext,
3
3
  runWithRequestContext
4
- } from "./index-e0a57ymn.js";
4
+ } from "./index-nemjkxjp.js";
5
5
  import {
6
6
  objectShapeKeys
7
7
  } from "./index-22by16v6.js";
@@ -13,7 +13,7 @@ import {
13
13
  import {
14
14
  isUnsafeKey,
15
15
  safeJsonParse
16
- } from "./index-hsz2mmk0.js";
16
+ } from "./index-vj3vvpaa.js";
17
17
  import {
18
18
  AppError,
19
19
  STITCH_ERROR_STATUS,
@@ -7,7 +7,7 @@ import {
7
7
  defaultSignalSource,
8
8
  guardSignalCallback,
9
9
  reportSignalError
10
- } from "./index-endzd8sj.js";
10
+ } from "./index-vbf2p6me.js";
11
11
  import {
12
12
  extractIp,
13
13
  getClientInfo,
@@ -18,7 +18,7 @@ import {
18
18
  runWithRequestContext,
19
19
  setRequestEndpoint,
20
20
  setRequestError
21
- } from "./index-e0a57ymn.js";
21
+ } from "./index-nemjkxjp.js";
22
22
  import {
23
23
  ShutdownOptionsSchema,
24
24
  isStreamCancellation,
@@ -39,7 +39,7 @@ import {
39
39
  joinRoutePath,
40
40
  parseTrailingWildcard,
41
41
  safeJsonParse
42
- } from "./index-hsz2mmk0.js";
42
+ } from "./index-vj3vvpaa.js";
43
43
  import {
44
44
  AppError,
45
45
  badRequest
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  isUnsafeKey,
3
3
  mergeMeta
4
- } from "./index-hsz2mmk0.js";
4
+ } from "./index-vj3vvpaa.js";
5
5
  import {
6
6
  callRuntimeHandler,
7
7
  isRecord,
@@ -13,6 +13,9 @@ import {
13
13
  AppError
14
14
  } from "./index-0w9abg87.js";
15
15
 
16
+ // src/application/kernel.ts
17
+ import { z } from "zod";
18
+
16
19
  // src/application/graph.ts
17
20
  function resolveResourceGraph(resources) {
18
21
  const entries = resources.map((resource, declarationIndex) => ({
@@ -82,6 +85,15 @@ var ApplicationShutdownBudgetSchema = ShutdownOptionsSchema.pick({
82
85
  gracePeriodMs: true,
83
86
  forceTimeoutMs: true
84
87
  });
88
+ var ApplicationRestartInputSchema = z.object({ resourceId: z.string().min(1) }).strict().readonly();
89
+ var ApplicationRestartOutcomeSchema = z.enum(["restarted", "failed", "refused"]);
90
+ var ApplicationRestartResultSchema = z.object({
91
+ resourceId: z.string(),
92
+ affected: z.array(z.string()).readonly(),
93
+ outcome: ApplicationRestartOutcomeSchema,
94
+ reason: z.string().optional(),
95
+ durationMs: z.number().int().nonnegative()
96
+ }).strict().readonly();
85
97
 
86
98
  class ApplicationAdmissionError extends AppError {
87
99
  constructor() {
@@ -294,138 +306,221 @@ function createApplication(config) {
294
306
  publish();
295
307
  }
296
308
  };
297
- const runStart = async () => {
298
- lifecycle = "starting";
299
- publish();
300
- let startFailure;
301
- try {
302
- for (const entry of ordered) {
303
- if (shutdownRequested || startupAbort.signal.aborted) {
309
+ const startEach = async (entries, signal) => {
310
+ for (const entry of entries) {
311
+ if (shutdownRequested || signal.aborted) {
312
+ throw new ApplicationStartupInterruptedError;
313
+ }
314
+ const record = records.get(entry.id);
315
+ if (!record)
316
+ throw new Error("Managed resource record disappeared");
317
+ const dependencyFailed = entry.dependsOn.some((dependencyId) => records.get(dependencyId)?.state !== "ready");
318
+ if (dependencyFailed) {
319
+ record.state = "failed";
320
+ record.health = "unhealthy";
321
+ record.failures.push("start");
322
+ publish();
323
+ if (entry.required) {
324
+ throw new Error(`[stitchkit] required resource "${entry.id}" has an unavailable dependency`);
325
+ }
326
+ continue;
327
+ }
328
+ record.attempted = true;
329
+ record.state = "starting";
330
+ publish();
331
+ try {
332
+ const started = await entry.resource.start(contextFor(record, { signal }));
333
+ if (shutdownRequested || signal.aborted) {
304
334
  throw new ApplicationStartupInterruptedError;
305
335
  }
306
- const record = records.get(entry.id);
307
- if (!record)
308
- throw new Error("Managed resource record disappeared");
309
- const dependencyFailed = entry.dependsOn.some((dependencyId) => records.get(dependencyId)?.state !== "ready");
310
- if (dependencyFailed) {
336
+ if (isStartResult(started)) {
337
+ record.runtime = started;
338
+ if (started.value !== undefined)
339
+ published.set(entry.id, started.value);
340
+ let resourceReady = started.ready === undefined;
341
+ let completionSettled = false;
342
+ let completionFailure;
343
+ const completion = started.completion?.then(() => {
344
+ completionSettled = true;
345
+ if (resourceReady)
346
+ markLateCompletion(record);
347
+ }, (error) => {
348
+ completionSettled = true;
349
+ completionFailure = error;
350
+ if (resourceReady)
351
+ markLateCompletion(record, { error });
352
+ });
353
+ if (started.ready && completion) {
354
+ const readiness = started.ready.then(() => "ready");
355
+ const completionBeforeReady = completion.then(() => "completion");
356
+ const first = await Promise.race([readiness, completionBeforeReady]);
357
+ if (first === "completion") {
358
+ throw new ResourceCompletionBeforeReadyError(entry.id, completionFailure);
359
+ }
360
+ resourceReady = true;
361
+ if (completionSettled) {
362
+ throw new ResourceCompletionBeforeReadyError(entry.id, completionFailure);
363
+ }
364
+ } else if (started.ready) {
365
+ await started.ready;
366
+ resourceReady = true;
367
+ } else if (completion) {}
368
+ }
369
+ if (shutdownRequested || signal.aborted) {
370
+ throw new ApplicationStartupInterruptedError;
371
+ }
372
+ record.state = "ready";
373
+ if (!record.healthReported) {
374
+ record.health = "healthy";
375
+ record.everHealthy = true;
376
+ }
377
+ publish();
378
+ } catch (error) {
379
+ const interrupted = shutdownRequested || signal.aborted;
380
+ if (!(error instanceof ApplicationStartupInterruptedError)) {
381
+ record.failures.push(error instanceof ResourceCompletionBeforeReadyError ? "completion" : record.runtime?.ready ? "ready" : "start");
311
382
  record.state = "failed";
312
383
  record.health = "unhealthy";
313
- record.failures.push("start");
384
+ reportFailure(entry.id, record.failures[record.failures.length - 1] ?? "start", error);
314
385
  publish();
315
- if (entry.required) {
316
- throw new Error(`[stitchkit] required resource "${entry.id}" has an unavailable dependency`);
317
- }
318
- continue;
319
386
  }
320
- record.attempted = true;
321
- record.state = "starting";
387
+ if (interrupted || entry.required)
388
+ throw error;
389
+ }
390
+ }
391
+ };
392
+ const activateEach = async (entries, signal) => {
393
+ for (const entry of entries) {
394
+ if (shutdownRequested || signal.aborted) {
395
+ throw new ApplicationStartupInterruptedError;
396
+ }
397
+ const record = records.get(entry.id);
398
+ if (record?.state !== "ready")
399
+ continue;
400
+ const dependencyUnavailable = entry.dependsOn.some((dependencyId) => {
401
+ const dependency = records.get(dependencyId);
402
+ return dependency?.state !== "ready" || !dependency.activated;
403
+ });
404
+ if (dependencyUnavailable) {
405
+ record.failures.push("start");
406
+ record.state = "failed";
407
+ record.health = "unhealthy";
322
408
  publish();
323
- try {
324
- const started = await entry.resource.start(contextFor(record, { signal: startupAbort.signal }));
325
- if (shutdownRequested || startupAbort.signal.aborted) {
326
- throw new ApplicationStartupInterruptedError;
327
- }
328
- if (isStartResult(started)) {
329
- record.runtime = started;
330
- if (started.value !== undefined)
331
- published.set(entry.id, started.value);
332
- let resourceReady = started.ready === undefined;
333
- let completionSettled = false;
334
- let completionFailure;
335
- const completion = started.completion?.then(() => {
336
- completionSettled = true;
337
- if (resourceReady)
338
- markLateCompletion(record);
339
- }, (error) => {
340
- completionSettled = true;
341
- completionFailure = error;
342
- if (resourceReady)
343
- markLateCompletion(record, { error });
344
- });
345
- if (started.ready && completion) {
346
- const readiness = started.ready.then(() => "ready");
347
- const completionBeforeReady = completion.then(() => "completion");
348
- const first = await Promise.race([readiness, completionBeforeReady]);
349
- if (first === "completion") {
350
- throw new ResourceCompletionBeforeReadyError(entry.id, completionFailure);
351
- }
352
- resourceReady = true;
353
- if (completionSettled) {
354
- throw new ResourceCompletionBeforeReadyError(entry.id, completionFailure);
355
- }
356
- } else if (started.ready) {
357
- await started.ready;
358
- resourceReady = true;
359
- } else if (completion) {}
360
- }
361
- if (shutdownRequested || startupAbort.signal.aborted) {
362
- throw new ApplicationStartupInterruptedError;
363
- }
364
- record.state = "ready";
365
- if (!record.healthReported) {
366
- record.health = "healthy";
367
- record.everHealthy = true;
368
- }
369
- publish();
370
- } catch (error) {
371
- const interrupted = shutdownRequested || startupAbort.signal.aborted;
372
- if (!(error instanceof ApplicationStartupInterruptedError)) {
373
- record.failures.push(error instanceof ResourceCompletionBeforeReadyError ? "completion" : record.runtime?.ready ? "ready" : "start");
374
- record.state = "failed";
375
- record.health = "unhealthy";
376
- reportFailure(entry.id, record.failures[record.failures.length - 1] ?? "start", error);
377
- publish();
378
- }
379
- if (interrupted || entry.required)
380
- throw error;
409
+ if (entry.required) {
410
+ throw new Error(`[stitchkit] required resource "${entry.id}" has an unavailable activation dependency`);
381
411
  }
412
+ continue;
382
413
  }
383
- lifecycle = "ready";
384
- publish();
385
- for (const entry of ordered) {
386
- if (shutdownRequested || startupAbort.signal.aborted) {
414
+ try {
415
+ await entry.resource.activate?.(contextFor(record));
416
+ if (shutdownRequested || signal.aborted) {
387
417
  throw new ApplicationStartupInterruptedError;
388
418
  }
389
- const record = records.get(entry.id);
390
- if (record?.state !== "ready")
391
- continue;
392
- const dependencyUnavailable = entry.dependsOn.some((dependencyId) => {
393
- const dependency = records.get(dependencyId);
394
- return dependency?.state !== "ready" || !dependency.activated;
395
- });
396
- if (dependencyUnavailable) {
419
+ record.activated = true;
420
+ if (entry.required && (record.state !== "ready" || record.health !== "healthy")) {
421
+ const observed = `${record.state}/${record.health}`;
422
+ throw new Error(record.everHealthy ? `[stitchkit] required resource "${entry.id}" lost readiness during activation (${observed})` : `[stitchkit] required resource "${entry.id}" is not healthy (${observed}). A required resource must be healthy for the application to be ready; a resource that is expected to start degraded belongs behind \`required: false\`.`);
423
+ }
424
+ } catch (error) {
425
+ const interrupted = shutdownRequested || signal.aborted;
426
+ if (!(error instanceof ApplicationStartupInterruptedError)) {
397
427
  record.failures.push("start");
398
428
  record.state = "failed";
399
429
  record.health = "unhealthy";
430
+ reportFailure(entry.id, "start", error);
400
431
  publish();
401
- if (entry.required) {
402
- throw new Error(`[stitchkit] required resource "${entry.id}" has an unavailable activation dependency`);
403
- }
404
- continue;
405
- }
406
- try {
407
- await entry.resource.activate?.(contextFor(record));
408
- if (shutdownRequested || startupAbort.signal.aborted) {
409
- throw new ApplicationStartupInterruptedError;
410
- }
411
- record.activated = true;
412
- if (entry.required && (record.state !== "ready" || record.health !== "healthy")) {
413
- const observed = `${record.state}/${record.health}`;
414
- throw new Error(record.everHealthy ? `[stitchkit] required resource "${entry.id}" lost readiness during activation (${observed})` : `[stitchkit] required resource "${entry.id}" is not healthy (${observed}). A required resource must be healthy for the application to be ready; a resource that is expected to start degraded belongs behind \`required: false\`.`);
415
- }
416
- } catch (error) {
417
- const interrupted = shutdownRequested || startupAbort.signal.aborted;
418
- if (!(error instanceof ApplicationStartupInterruptedError)) {
419
- record.failures.push("start");
420
- record.state = "failed";
421
- record.health = "unhealthy";
422
- reportFailure(entry.id, "start", error);
423
- publish();
424
- }
425
- if (interrupted || entry.required)
426
- throw error;
427
432
  }
433
+ if (interrupted || entry.required)
434
+ throw error;
435
+ }
436
+ }
437
+ };
438
+ const subtreeOf = (resourceId) => {
439
+ const affected = new Set([resourceId]);
440
+ for (const entry of ordered) {
441
+ if (entry.dependsOn.some((dependencyId) => affected.has(dependencyId))) {
442
+ affected.add(entry.id);
428
443
  }
444
+ }
445
+ return ordered.filter((entry) => affected.has(entry.id));
446
+ };
447
+ const closeOne = async (entry, record, signal) => {
448
+ const context = () => contextFor(record, { signal });
449
+ if (record.attempted && !record.closed) {
450
+ await entry.resource.stopAdmission?.(context());
451
+ await entry.resource.drain?.(context());
452
+ record.closeInvoked = true;
453
+ await entry.resource.close?.(context());
454
+ }
455
+ record.closed = false;
456
+ record.closeInvoked = false;
457
+ record.attempted = false;
458
+ record.activated = false;
459
+ record.state = "registered";
460
+ record.health = "unknown";
461
+ record.healthReported = false;
462
+ record.runtime = undefined;
463
+ published.delete(entry.id);
464
+ };
465
+ let restarting = Promise.resolve();
466
+ const runRestart = async (input) => {
467
+ const startedAt = Date.now();
468
+ const parsed = ApplicationRestartInputSchema.parse(input);
469
+ const affected = subtreeOf(parsed.resourceId);
470
+ const affectedIds = affected.map((entry) => entry.id);
471
+ const refuse = (reason) => ({
472
+ resourceId: parsed.resourceId,
473
+ affected: affectedIds,
474
+ outcome: "refused",
475
+ reason,
476
+ durationMs: Date.now() - startedAt
477
+ });
478
+ if (!records.has(parsed.resourceId)) {
479
+ return refuse(`no resource is registered as "${parsed.resourceId}"`);
480
+ }
481
+ if (shutdownRequested) {
482
+ return refuse("the application is shutting down");
483
+ }
484
+ if (lifecycle !== "ready") {
485
+ return refuse(`the application is ${lifecycle}, not ready`);
486
+ }
487
+ const restartAbort = new AbortController;
488
+ try {
489
+ for (const entry of [...affected].reverse()) {
490
+ const record = records.get(entry.id);
491
+ if (record)
492
+ await closeOne(entry, record, restartAbort.signal);
493
+ }
494
+ publish();
495
+ await startEach(affected, restartAbort.signal);
496
+ await activateEach(affected, restartAbort.signal);
497
+ publish();
498
+ return {
499
+ resourceId: parsed.resourceId,
500
+ affected: affectedIds,
501
+ outcome: "restarted",
502
+ durationMs: Date.now() - startedAt
503
+ };
504
+ } catch (error) {
505
+ publish();
506
+ return {
507
+ resourceId: parsed.resourceId,
508
+ affected: affectedIds,
509
+ outcome: "failed",
510
+ reason: error instanceof Error ? error.message : String(error),
511
+ durationMs: Date.now() - startedAt
512
+ };
513
+ }
514
+ };
515
+ const runStart = async () => {
516
+ lifecycle = "starting";
517
+ publish();
518
+ let startFailure;
519
+ try {
520
+ await startEach(ordered, startupAbort.signal);
521
+ lifecycle = "ready";
522
+ publish();
523
+ await activateEach(ordered, startupAbort.signal);
429
524
  if (shutdownRequested) {
430
525
  throw new ApplicationStartupInterruptedError;
431
526
  }
@@ -714,8 +809,17 @@ function createApplication(config) {
714
809
  listener(snapshot());
715
810
  return () => listeners.delete(listener);
716
811
  },
717
- shutdown
812
+ shutdown,
813
+ restart(input) {
814
+ const queued = restarting.then(() => runRestart(input));
815
+ restarting = queued.then(() => {
816
+ return;
817
+ }, () => {
818
+ return;
819
+ });
820
+ return queued;
821
+ }
718
822
  };
719
823
  }
720
824
 
721
- export { ApplicationShutdownOptionsSchema, ApplicationShutdownBudgetSchema, ApplicationAdmissionError, createApplication };
825
+ export { ApplicationShutdownOptionsSchema, ApplicationShutdownBudgetSchema, ApplicationRestartInputSchema, ApplicationRestartOutcomeSchema, ApplicationRestartResultSchema, ApplicationAdmissionError, createApplication };
@@ -272,6 +272,22 @@ function mergeMeta(contractMeta, endpointMeta) {
272
272
  import { z as z2 } from "zod";
273
273
  // src/contract/pagination.ts
274
274
  import { z as z3 } from "zod";
275
+
276
+ // src/internal/base64url.ts
277
+ function bytesToBase64Url(bytes) {
278
+ let binary = "";
279
+ for (const byte of bytes)
280
+ binary += String.fromCharCode(byte);
281
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
282
+ }
283
+ function base64UrlToBytes(segment) {
284
+ if (!/^[A-Za-z0-9_-]*$/.test(segment) || segment.length % 4 === 1) {
285
+ throw new Error("invalid base64url segment");
286
+ }
287
+ const b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
288
+ const padded = b64.padEnd(Math.ceil(b64.length / 4) * 4, "=");
289
+ return Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
290
+ }
275
291
  // src/contract/stream.ts
276
292
  import { z as z4 } from "zod";
277
293
  var DEFAULT_CONTRACT_STREAM_FRAME_BYTES = 256 * 1024;
@@ -288,4 +304,4 @@ var ContractStreamFrameSchema = z4.discriminatedUnion("type", [
288
304
  }).strict(),
289
305
  z4.object({ type: z4.literal("end") }).strict()
290
306
  ]);
291
- export { joinRoutePath, parseTrailingWildcard, isUnsafeKey, safeJsonParse, defineContract, mergeMeta, DEFAULT_CONTRACT_STREAM_FRAME_BYTES, ContractStreamFrameSchema };
307
+ export { joinRoutePath, parseTrailingWildcard, isUnsafeKey, safeJsonParse, defineContract, mergeMeta, bytesToBase64Url, base64UrlToBytes, DEFAULT_CONTRACT_STREAM_FRAME_BYTES, ContractStreamFrameSchema };
package/dist/index.js CHANGED
@@ -27,8 +27,7 @@ import {
27
27
  parseTrailingWildcard,
28
28
  rateLimited,
29
29
  unauthorized
30
- } from "./index-4kp4fxvp.js";
31
- import"./index-rxfy4cq7.js";
30
+ } from "./index-f6n5n7nz.js";
32
31
  import {
33
32
  isRecord,
34
33
  mapObject,
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * One answer to "may this happen?", from one answerer.
4
+ *
5
+ * Three outcomes and no fourth, because the third is the one that gets left out
6
+ * and then invented: `defer` means *not my call*, which is a different statement
7
+ * from `allow`. Collapsing them makes every answerer that had no opinion into an
8
+ * answerer that approved, and nothing in the result says which happened.
9
+ *
10
+ * `deny` carries its reason as a required field. A refusal without one reaches a
11
+ * human as "denied" and sends them to read the policy source to find out what
12
+ * they did, which is the moment a policy engine stops being worth having.
13
+ *
14
+ * Browser-safe and shared on purpose: an event listener voting on a topic
15
+ * (`stitchkit/live`) and a policy in an ordered pipeline
16
+ * (`stitchkit/application`) are different mechanisms answering the same
17
+ * question, and one question with two vocabularies is a codebase where `grep`
18
+ * finds half the answerers.
19
+ */
20
+ export declare const PolicyDecisionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
21
+ outcome: z.ZodLiteral<"allow">;
22
+ }, z.core.$strict>, z.ZodObject<{
23
+ outcome: z.ZodLiteral<"deny">;
24
+ reason: z.ZodString;
25
+ }, z.core.$strict>, z.ZodObject<{
26
+ outcome: z.ZodLiteral<"defer">;
27
+ }, z.core.$strict>], "outcome">;
28
+ export type PolicyDecision = z.infer<typeof PolicyDecisionSchema>;
29
+ /** What is concluded when nobody claimed the question — see each mechanism for which it uses. */
30
+ export type UndecidedOutcome = 'allow' | 'deny';
31
+ //# sourceMappingURL=decision.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decision.d.ts","sourceRoot":"","sources":["../../src/internal/decision.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,oBAAoB;;;;;;;+BAI/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,iGAAiG;AACjG,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,MAAM,CAAC"}
@@ -1,3 +1,38 @@
1
+ /**
2
+ * A stable identity for a call's arguments — one implementation, two callers.
3
+ *
4
+ * Two callers need the same thing for different reasons. The MCP round keys a
5
+ * tool invocation by `(operation, arguments)` so a retry is recognised as the
6
+ * same round; a watched read keys its shared source by `(operation, arguments)`
7
+ * so two browsers asking the same question are one read on the server. If those
8
+ * two computed the key differently, the same arguments would be the same round
9
+ * and two different watches — and nothing would say so.
10
+ *
11
+ * The stability that matters is key order: `{a:1,b:2}` and `{b:2,a:1}` are the
12
+ * same arguments and `JSON.stringify` disagrees, so a naive key silently splits
13
+ * one shared source into two whenever a caller builds its object in a different
14
+ * order. That is not a hypothetical: object literal order follows the source
15
+ * that wrote it, and two components asking the same question rarely share one.
16
+ *
17
+ * ## Why this is not SHA-256
18
+ *
19
+ * It was, and that made every watched read fail on an ordinary intranet. Web
20
+ * Crypto — `crypto.subtle` — exists only in a **secure context**, so a page
21
+ * served over plain HTTP from a LAN name has no `crypto.subtle` at all and the
22
+ * first digest throws `Cannot read properties of undefined`. `localhost` is
23
+ * secure by definition, which is exactly why nothing caught it until a browser
24
+ * opened the app by its name.
25
+ *
26
+ * The fix is not a fallback, it is admitting what this is for: **identity, not
27
+ * secrecy**. Nothing here resists an adversary — an attacker who can choose your
28
+ * watch arguments can already ask the question directly. So the requirement is
29
+ * distribution and determinism, both ends agreeing, and no ambient capability;
30
+ * a cryptographic hash bought none of that and cost the entire non-secure web.
31
+ *
32
+ * Being synchronous is the second thing it buys. A promise for a key made the
33
+ * first subscription of a question asynchronous, which meant a component could
34
+ * not be handed a retained value in the same turn it subscribed.
35
+ */
1
36
  /**
2
37
  * The value with every object's keys sorted, recursively — arrays keep their
3
38
  * order, because in an array order *is* the value.
@@ -7,12 +42,11 @@
7
42
  */
8
43
  export declare function stableValue(value: unknown): unknown;
9
44
  /**
10
- * A bounded, order-independent digest of a call's arguments.
45
+ * A bounded, order-independent identity for a call's arguments.
11
46
  *
12
- * SHA-256 over the key-sorted JSON, base64url without padding. Bounded because
13
- * the key travels on the wire and arguments do not have a bounded size; a hash
14
- * rather than the JSON itself so a large argument object does not become a
15
- * large subscription key repeated in every frame.
47
+ * 128 bits as 32 lowercase hex characters. Deterministic across runtimes and
48
+ * across both ends of a socket, synchronous, and dependent on no ambient
49
+ * capability see the module header for why that last one is the whole point.
16
50
  *
17
51
  * What it cannot do, stated because the limit is easy to walk into: values
18
52
  * `JSON.stringify` drops or transforms — `undefined` members, a `Date`, a `Map`,
@@ -20,5 +54,5 @@ export declare function stableValue(value: unknown): unknown;
20
54
  * Contract arguments are parsed JSON, so this is the argument shape by
21
55
  * construction; a caller digesting something else has to say what it means.
22
56
  */
23
- export declare function argumentsDigest(args: Record<string, unknown>): Promise<string>;
57
+ export declare function argumentsDigest(args: Record<string, unknown>): string;
24
58
  //# sourceMappingURL=stable-digest.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"stable-digest.d.ts","sourceRoot":"","sources":["../../src/internal/stable-digest.ts"],"names":[],"mappings":"AAkBA;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQnD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAGpF"}
1
+ {"version":3,"file":"stable-digest.d.ts","sourceRoot":"","sources":["../../src/internal/stable-digest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQnD;AAyCD;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAGrE"}