@agent-native/core 0.228.2-nightly-20261113032548 → 0.228.2

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 (42) hide show
  1. package/dist/application-state/handlers.d.ts +2 -2
  2. package/dist/cli/skills-content/an-skill.d.ts +1 -1
  3. package/dist/cli/skills-content/an-skill.js +16 -0
  4. package/dist/collab/routes.d.ts +1 -1
  5. package/dist/db/schema-snapshot.generated.js +2 -1
  6. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  7. package/dist/file-upload/builder-gateway.d.ts +42 -0
  8. package/dist/file-upload/builder-gateway.js +185 -0
  9. package/dist/file-upload/builder.js +8 -11
  10. package/dist/file-upload/index.d.ts +1 -0
  11. package/dist/file-upload/index.js +1 -0
  12. package/dist/framework-package-versions.json +11 -11
  13. package/dist/identity/rekey.js +2 -0
  14. package/dist/mcp/embed-app.js +259 -3
  15. package/dist/mcp/oauth-route.js +7 -0
  16. package/dist/mcp/screen-memory-stdio.d.ts +3 -3
  17. package/dist/notifications/routes.d.ts +3 -3
  18. package/dist/observability/routes.d.ts +5 -5
  19. package/dist/org/index.d.ts +1 -1
  20. package/dist/secrets/api-keys.js +8 -7
  21. package/dist/secrets/index.d.ts +2 -1
  22. package/dist/secrets/index.js +2 -1
  23. package/dist/secrets/routes.d.ts +3 -3
  24. package/dist/secrets/routes.js +7 -8
  25. package/dist/secrets/schema.d.ts +51 -0
  26. package/dist/secrets/schema.js +3 -0
  27. package/dist/secrets/storage.d.ts +83 -1
  28. package/dist/secrets/storage.js +194 -6
  29. package/dist/secrets/usage.js +2 -4
  30. package/dist/secrets/vault-managed.d.ts +17 -0
  31. package/dist/secrets/vault-managed.js +19 -0
  32. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  33. package/dist/server/builder-signup-experiment.d.ts +4 -4
  34. package/dist/server/credential-provider.js +7 -7
  35. package/dist/server/first-touch-route.d.ts +1 -1
  36. package/dist/server/transcribe-voice.d.ts +1 -1
  37. package/dist/templates/workspace-core/.agents/skills/external-agents/references/mcp-apps-embedding.md +11 -0
  38. package/docs/content/security.mdx +1 -1
  39. package/docs/content/template-dispatch-developers.mdx +7 -7
  40. package/docs/content/template-dispatch-reference.mdx +16 -10
  41. package/package.json +4 -4
  42. package/src/templates/workspace-core/.agents/skills/external-agents/references/mcp-apps-embedding.md +11 -0
@@ -18,9 +18,10 @@
18
18
  * Secret values are NEVER logged and NEVER returned from any route handler.
19
19
  */
20
20
  import type { SecretScope } from "./register.js";
21
+ import { VAULT_SYNC_DESCRIPTION_PREFIX } from "./vault-managed.js";
22
+ export { VAULT_SYNC_DESCRIPTION_PREFIX };
21
23
  export declare const appSecretsStore: import("../db/store-registry.js").Store;
22
24
  export declare function ensureTable(): Promise<void>;
23
- export declare const VAULT_SYNC_DESCRIPTION_PREFIX = "Synced from Dispatch vault:";
24
25
  export declare function last4(value: string): string;
25
26
  export interface SecretRef {
26
27
  key: string;
@@ -31,6 +32,19 @@ export interface WriteSecretArgs extends SecretRef {
31
32
  value: string;
32
33
  description?: string;
33
34
  urlAllowlist?: string;
35
+ /** Display name. A write that omits it keeps the stored one. */
36
+ label?: string | null;
37
+ /** Provider tag. A write that omits it keeps the stored one. */
38
+ provider?: string | null;
39
+ /**
40
+ * Who created the key through the Dispatch Vault, which marks the row as
41
+ * Vault-managed. The first creator is kept: later writes never replace it.
42
+ */
43
+ createdBy?: string | null;
44
+ }
45
+ export declare class AppSecretKeyConflictError extends Error {
46
+ readonly statusCode = 409;
47
+ constructor(key: string);
34
48
  }
35
49
  export declare class AppSecretKeyMismatchError extends Error {
36
50
  constructor(ref: SecretRef);
@@ -75,12 +89,29 @@ export declare function getAppSecretMeta(ref: SecretRef): Promise<{
75
89
  updatedAt: number;
76
90
  } | null>;
77
91
  export interface SecretMeta {
92
+ id: string;
78
93
  key: string;
79
94
  scope: SecretScope;
80
95
  scopeId: string;
81
96
  last4: string;
82
97
  description: string | null;
83
98
  urlAllowlist: string[] | null;
99
+ label: string | null;
100
+ provider: string | null;
101
+ createdBy: string | null;
102
+ createdAt: number;
103
+ updatedAt: number;
104
+ }
105
+ /** A stored key's metadata with nothing derived from its value. */
106
+ export interface AppSecretSummary {
107
+ id: string;
108
+ key: string;
109
+ scope: SecretScope;
110
+ scopeId: string;
111
+ description: string | null;
112
+ label: string | null;
113
+ provider: string | null;
114
+ createdBy: string | null;
84
115
  createdAt: number;
85
116
  updatedAt: number;
86
117
  }
@@ -97,6 +128,57 @@ export declare function readAppSecretMeta(ref: SecretRef): Promise<SecretMeta |
97
128
  * user-created keys.
98
129
  */
99
130
  export declare function listAppSecretsForScope(scope: SecretScope, scopeId: string): Promise<SecretMeta[]>;
131
+ /** The key's metadata by stored id, within one scope. Null when that scope holds no such row. */
132
+ export declare function readAppSecretMetaById(target: {
133
+ id: string;
134
+ scope: SecretScope;
135
+ scopeId: string;
136
+ }): Promise<SecretMeta | null>;
137
+ /**
138
+ * Every key in one scope with nothing derived from its value, so no
139
+ * decryption: the cheap read for pickers, counts and lookups. Use
140
+ * `listAppSecretsForScope` when the masked preview is needed.
141
+ */
142
+ export declare function listAppSecretSummaries(scope: SecretScope, scopeId: string): Promise<AppSecretSummary[]>;
143
+ export interface UpdateAppSecretPatch {
144
+ /** Rename the key; the row keeps its id, value and creator. */
145
+ key?: string;
146
+ /** Replace the value. Omit to leave the stored ciphertext untouched. */
147
+ value?: string;
148
+ label?: string | null;
149
+ provider?: string | null;
150
+ description?: string | null;
151
+ /**
152
+ * The Dispatch Vault is writing this row, so it is Vault-managed from now
153
+ * on. Recorded as the creator only when the row has none.
154
+ */
155
+ adoptedBy?: string;
156
+ }
157
+ /**
158
+ * Edit one stored key by its id within a scope, in one statement: rename,
159
+ * replace the value, and change the display fields together or alone. A field
160
+ * the patch leaves out keeps its stored value; `null` clears `label`,
161
+ * `provider` and `description`. Returns the key now stored and the one it
162
+ * replaced, or null when that scope holds no row with this id.
163
+ */
164
+ export declare function updateAppSecretById(target: {
165
+ id: string;
166
+ scope: SecretScope;
167
+ scopeId: string;
168
+ }, patch: UpdateAppSecretPatch): Promise<{
169
+ key: string;
170
+ previousKey: string;
171
+ } | null>;
172
+ /**
173
+ * Delete one row by its id within a scope. Unlike `deleteAppSecret` this cannot
174
+ * remove a different row that took over the key name after a rename. Returns
175
+ * whether a row was removed.
176
+ */
177
+ export declare function deleteAppSecretById(target: {
178
+ id: string;
179
+ scope: SecretScope;
180
+ scopeId: string;
181
+ }): Promise<boolean>;
100
182
  export declare function deleteAppSecret(ref: SecretRef): Promise<boolean>;
101
183
  /**
102
184
  * Delete only if the stored ciphertext is still the version the caller read.
@@ -29,6 +29,8 @@ import { encryptSecretValue as encryptLegacyValue, encryptSharedSecretValue as e
29
29
  import { AppSecretDecryptionError } from "./errors.js";
30
30
  import { invalidateOptionalKeyCache } from "./optional-key-cache.js";
31
31
  import { APP_SECRETS_CREATE_SQL } from "./schema.js";
32
+ import { VAULT_SYNC_DESCRIPTION_PREFIX } from "./vault-managed.js";
33
+ export { VAULT_SYNC_DESCRIPTION_PREFIX };
32
34
  export const appSecretsStore = defineStore({
33
35
  id: "app_secrets",
34
36
  migrations: [
@@ -42,12 +44,19 @@ export const appSecretsStore = defineStore({
42
44
  await ensureColumnExists("app_secrets", "shared_encrypted_value", `ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS shared_encrypted_value TEXT`);
43
45
  },
44
46
  },
47
+ {
48
+ name: "vault-metadata-columns",
49
+ run: async () => {
50
+ await ensureColumnExists("app_secrets", "label", `ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS label TEXT`);
51
+ await ensureColumnExists("app_secrets", "provider", `ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS provider TEXT`);
52
+ await ensureColumnExists("app_secrets", "created_by", `ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS created_by TEXT`);
53
+ },
54
+ },
45
55
  ],
46
56
  });
47
57
  export function ensureTable() {
48
58
  return appSecretsStore.ready();
49
59
  }
50
- export const VAULT_SYNC_DESCRIPTION_PREFIX = "Synced from Dispatch vault:";
51
60
  export function last4(value) {
52
61
  if (!value)
53
62
  return "";
@@ -55,6 +64,13 @@ export function last4(value) {
55
64
  return "••••";
56
65
  return "••••" + value.slice(-4);
57
66
  }
67
+ export class AppSecretKeyConflictError extends Error {
68
+ statusCode = 409;
69
+ constructor(key) {
70
+ super(`Credential key "${key}" is already in use`);
71
+ this.name = "AppSecretKeyConflictError";
72
+ }
73
+ }
58
74
  export class AppSecretKeyMismatchError extends Error {
59
75
  constructor(ref) {
60
76
  super(`Refusing to write secret "${ref.key}" (${ref.scope} scope): this development process is connected to a shared database, and the secrets stored there use encryption key material it does not prefer. Set WORKSPACE_SECRETS_ENCRYPTION_KEY to match the deployments that use this database, or point DATABASE_URL at a local database.`);
@@ -102,7 +118,7 @@ async function assertWriteKeepsSecretsReadable(ref) {
102
118
  */
103
119
  export async function writeAppSecret(args) {
104
120
  await ensureTable();
105
- const { key, value, scope, scopeId, description, urlAllowlist } = args;
121
+ const { key, value, scope, scopeId, description, urlAllowlist, label, provider, createdBy, } = args;
106
122
  if (!key || !value || !scope || !scopeId) {
107
123
  throw new Error("writeAppSecret: key, value, scope, and scopeId are all required");
108
124
  }
@@ -138,13 +154,20 @@ export async function writeAppSecret(args) {
138
154
  // `populateSharedAppSecret`, which fills a NULL column or compare-and-swap
139
155
  // replaces the exact legacy ciphertext it just decrypted. A temporary miss
140
156
  // is safer than serving rotated-away plaintext.
141
- const upsertSql = `INSERT INTO app_secrets (id, scope, scope_id, key, encrypted_value, shared_encrypted_value, description, url_allowlist, created_at, updated_at)
142
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
157
+ //
158
+ // label, provider and created_by follow the opposite rule: a write that does
159
+ // not pass them (every writer but the Dispatch Vault) leaves the stored
160
+ // values alone, and the first creator is never replaced.
161
+ const upsertSql = `INSERT INTO app_secrets (id, scope, scope_id, key, encrypted_value, shared_encrypted_value, description, url_allowlist, label, provider, created_by, created_at, updated_at)
162
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
143
163
  ON CONFLICT (scope, scope_id, key) DO UPDATE SET
144
164
  encrypted_value = excluded.encrypted_value,
145
165
  shared_encrypted_value = excluded.shared_encrypted_value,
146
166
  description = excluded.description,
147
167
  url_allowlist = COALESCE(excluded.url_allowlist, app_secrets.url_allowlist),
168
+ label = COALESCE(excluded.label, app_secrets.label),
169
+ provider = COALESCE(excluded.provider, app_secrets.provider),
170
+ created_by = COALESCE(app_secrets.created_by, excluded.created_by),
148
171
  updated_at = excluded.updated_at`;
149
172
  const upsertArgs = [
150
173
  id,
@@ -155,6 +178,9 @@ export async function writeAppSecret(args) {
155
178
  sharedEncrypted,
156
179
  description ?? null,
157
180
  urlAllowlist ?? null,
181
+ label ?? null,
182
+ provider ?? null,
183
+ createdBy ?? null,
158
184
  now,
159
185
  now,
160
186
  ];
@@ -173,6 +199,7 @@ export async function updateAppSecretUrlAllowlist(ref, origins) {
173
199
  const { rows } = await getDbExec().execute({
174
200
  sql: `UPDATE app_secrets SET url_allowlist = ?, updated_at = ?
175
201
  WHERE scope = ? AND scope_id = ? AND key = ?
202
+ AND created_by IS NULL
176
203
  AND (description IS NULL OR description NOT LIKE ?)
177
204
  RETURNING id`,
178
205
  args: [
@@ -494,7 +521,7 @@ export async function readAppSecretMeta(ref) {
494
521
  const { key, scope, scopeId } = ref;
495
522
  const client = getDbExec();
496
523
  const { rows } = await client.execute({
497
- sql: `SELECT id, encrypted_value, shared_encrypted_value, description, url_allowlist, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ? LIMIT 1`,
524
+ sql: `SELECT id, encrypted_value, shared_encrypted_value, description, url_allowlist, label, provider, created_by, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ? LIMIT 1`,
498
525
  args: [scope, scopeId, key],
499
526
  });
500
527
  if (rows.length === 0)
@@ -513,12 +540,16 @@ export async function readAppSecretMeta(ref) {
513
540
  last4Value = "";
514
541
  }
515
542
  return {
543
+ id: String(row.id),
516
544
  key,
517
545
  scope,
518
546
  scopeId,
519
547
  last4: last4Value,
520
548
  description: row.description ?? null,
521
549
  urlAllowlist: parseAllowlist(row.url_allowlist),
550
+ label: row.label ?? null,
551
+ provider: row.provider ?? null,
552
+ createdBy: row.created_by ?? null,
522
553
  createdAt: Number(row.created_at ?? 0),
523
554
  updatedAt: Number(row.updated_at ?? 0),
524
555
  };
@@ -532,7 +563,7 @@ export async function listAppSecretsForScope(scope, scopeId) {
532
563
  await ensureTable();
533
564
  const client = getDbExec();
534
565
  const { rows } = await client.execute({
535
- sql: `SELECT id, key, encrypted_value, shared_encrypted_value, description, url_allowlist, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? ORDER BY updated_at DESC`,
566
+ sql: `SELECT id, key, encrypted_value, shared_encrypted_value, description, url_allowlist, label, provider, created_by, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? ORDER BY updated_at DESC`,
536
567
  args: [scope, scopeId],
537
568
  });
538
569
  const results = [];
@@ -550,18 +581,175 @@ export async function listAppSecretsForScope(scope, scopeId) {
550
581
  last4Value = "";
551
582
  }
552
583
  results.push({
584
+ id: String(row.id),
553
585
  key: row.key,
554
586
  scope,
555
587
  scopeId,
556
588
  last4: last4Value,
557
589
  description: row.description ?? null,
558
590
  urlAllowlist: parseAllowlist(row.url_allowlist),
591
+ label: row.label ?? null,
592
+ provider: row.provider ?? null,
593
+ createdBy: row.created_by ?? null,
559
594
  createdAt: Number(row.created_at ?? 0),
560
595
  updatedAt: Number(row.updated_at ?? 0),
561
596
  });
562
597
  }
563
598
  return results;
564
599
  }
600
+ /** The key's metadata by stored id, within one scope. Null when that scope holds no such row. */
601
+ export async function readAppSecretMetaById(target) {
602
+ await ensureTable();
603
+ const { rows } = await getDbExec().execute({
604
+ sql: `SELECT key FROM app_secrets WHERE id = ? AND scope = ? AND scope_id = ? LIMIT 1`,
605
+ args: [target.id, target.scope, target.scopeId],
606
+ });
607
+ if (rows.length === 0)
608
+ return null;
609
+ return readAppSecretMeta({
610
+ key: String(rows[0].key),
611
+ scope: target.scope,
612
+ scopeId: target.scopeId,
613
+ });
614
+ }
615
+ /**
616
+ * Every key in one scope with nothing derived from its value, so no
617
+ * decryption: the cheap read for pickers, counts and lookups. Use
618
+ * `listAppSecretsForScope` when the masked preview is needed.
619
+ */
620
+ export async function listAppSecretSummaries(scope, scopeId) {
621
+ await ensureTable();
622
+ const { rows } = await getDbExec().execute({
623
+ sql: `SELECT id, key, description, label, provider, created_by, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? ORDER BY updated_at DESC`,
624
+ args: [scope, scopeId],
625
+ });
626
+ return rows.map((row) => ({
627
+ id: String(row.id),
628
+ key: String(row.key),
629
+ scope,
630
+ scopeId,
631
+ description: row.description ?? null,
632
+ label: row.label ?? null,
633
+ provider: row.provider ?? null,
634
+ createdBy: row.created_by ?? null,
635
+ createdAt: Number(row.created_at ?? 0),
636
+ updatedAt: Number(row.updated_at ?? 0),
637
+ }));
638
+ }
639
+ /**
640
+ * Edit one stored key by its id within a scope, in one statement: rename,
641
+ * replace the value, and change the display fields together or alone. A field
642
+ * the patch leaves out keeps its stored value; `null` clears `label`,
643
+ * `provider` and `description`. Returns the key now stored and the one it
644
+ * replaced, or null when that scope holds no row with this id.
645
+ */
646
+ export async function updateAppSecretById(target, patch) {
647
+ await ensureTable();
648
+ const client = getDbExec();
649
+ const { rows } = await client.execute({
650
+ sql: `SELECT key FROM app_secrets WHERE id = ? AND scope = ? AND scope_id = ? LIMIT 1`,
651
+ args: [target.id, target.scope, target.scopeId],
652
+ });
653
+ if (rows.length === 0)
654
+ return null;
655
+ const previousKey = String(rows[0].key);
656
+ const key = patch.key ?? previousKey;
657
+ if (!key)
658
+ throw new Error("updateAppSecretById: key cannot be empty");
659
+ if (patch.value !== undefined && !patch.value) {
660
+ throw new Error("updateAppSecretById: value cannot be empty");
661
+ }
662
+ if (key !== previousKey) {
663
+ const { rows: clash } = await client.execute({
664
+ sql: `SELECT 1 FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ? LIMIT 1`,
665
+ args: [target.scope, target.scopeId, key],
666
+ });
667
+ if (clash.length > 0)
668
+ throw new AppSecretKeyConflictError(key);
669
+ }
670
+ let encrypted = null;
671
+ let sharedEncrypted = null;
672
+ if (patch.value !== undefined) {
673
+ await assertWriteKeepsSecretsReadable({
674
+ key,
675
+ scope: target.scope,
676
+ scopeId: target.scopeId,
677
+ });
678
+ encrypted = encryptLegacyValue(patch.value);
679
+ sharedEncrypted = hasSharedSecretEncryptionKeyMaterial()
680
+ ? encryptValue(patch.value)
681
+ : null;
682
+ }
683
+ // Each CASE flag says whether the patch carries that field: a field left out
684
+ // keeps its stored value, and an explicit null clears it.
685
+ const { rows: updated } = await client.execute({
686
+ sql: `UPDATE app_secrets SET
687
+ key = CASE WHEN ? THEN ? ELSE key END,
688
+ encrypted_value = CASE WHEN ? THEN ? ELSE encrypted_value END,
689
+ shared_encrypted_value = CASE WHEN ? THEN ? ELSE shared_encrypted_value END,
690
+ label = CASE WHEN ? THEN ? ELSE label END,
691
+ provider = CASE WHEN ? THEN ? ELSE provider END,
692
+ description = CASE WHEN ? THEN ? ELSE description END,
693
+ created_by = COALESCE(created_by, ?),
694
+ updated_at = ?
695
+ WHERE id = ? AND scope = ? AND scope_id = ?
696
+ RETURNING id`,
697
+ args: [
698
+ patch.key !== undefined,
699
+ patch.key ?? null,
700
+ patch.value !== undefined,
701
+ encrypted,
702
+ patch.value !== undefined,
703
+ sharedEncrypted,
704
+ patch.label !== undefined,
705
+ patch.label ?? null,
706
+ patch.provider !== undefined,
707
+ patch.provider ?? null,
708
+ patch.description !== undefined,
709
+ patch.description ?? null,
710
+ patch.adoptedBy ?? null,
711
+ Date.now(),
712
+ target.id,
713
+ target.scope,
714
+ target.scopeId,
715
+ ],
716
+ });
717
+ if (updated.length === 0)
718
+ return null;
719
+ for (const staleKey of new Set([previousKey, key])) {
720
+ invalidateRequestSecret({
721
+ key: staleKey,
722
+ scope: target.scope,
723
+ scopeId: target.scopeId,
724
+ });
725
+ }
726
+ invalidateOptionalKeyCache();
727
+ invalidateAgentEngineStatusCache();
728
+ return { key, previousKey };
729
+ }
730
+ /**
731
+ * Delete one row by its id within a scope. Unlike `deleteAppSecret` this cannot
732
+ * remove a different row that took over the key name after a rename. Returns
733
+ * whether a row was removed.
734
+ */
735
+ export async function deleteAppSecretById(target) {
736
+ await ensureTable();
737
+ const client = getDbExec();
738
+ const { rows } = await client.execute({
739
+ sql: `DELETE FROM app_secrets WHERE id = ? AND scope = ? AND scope_id = ? RETURNING key`,
740
+ args: [target.id, target.scope, target.scopeId],
741
+ });
742
+ if (rows.length === 0)
743
+ return false;
744
+ invalidateRequestSecret({
745
+ key: String(rows[0].key),
746
+ scope: target.scope,
747
+ scopeId: target.scopeId,
748
+ });
749
+ invalidateOptionalKeyCache();
750
+ invalidateAgentEngineStatusCache();
751
+ return true;
752
+ }
565
753
  function parseAllowlist(raw) {
566
754
  if (raw === null)
567
755
  return null;
@@ -10,7 +10,7 @@ import { AGENT_PROVIDER_CATALOG, providerIdForEngine, } from "../client/agent-pr
10
10
  import { getRequestOrgId } from "../server/request-context.js";
11
11
  import { managedSecretUsage, resolveSecretManagedBy } from "./managed-keys.js";
12
12
  import { getRegisteredSecretUsage, getRequiredSecret, } from "./register.js";
13
- import { VAULT_SYNC_DESCRIPTION_PREFIX } from "./storage.js";
13
+ import { isVaultManagedSecret } from "./vault-managed.js";
14
14
  /** App id used in effects that apply to every app. */
15
15
  export const ALL_APPS = "all";
16
16
  function providerForKey(key) {
@@ -73,9 +73,7 @@ async function resolveSharedFallback(key) {
73
73
  : null;
74
74
  return {
75
75
  status: "shared",
76
- source: meta?.description?.startsWith(VAULT_SYNC_DESCRIPTION_PREFIX)
77
- ? "vault"
78
- : shared.source,
76
+ source: isVaultManagedSecret(meta) ? "vault" : shared.source,
79
77
  };
80
78
  }
81
79
  function engineDisplayLabel(entry) {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Marker on `app_secrets.description` that releases before the Vault moved
3
+ * onto `app_secrets` wrote on every Vault-owned row. New Vault writes mark a
4
+ * row with `created_by` instead: such a release deletes marked rows that its
5
+ * own Vault table has no entry for, and it must never find a new key that way.
6
+ */
7
+ export declare const VAULT_SYNC_DESCRIPTION_PREFIX = "Synced from Dispatch vault:";
8
+ /**
9
+ * True for a key the Dispatch Vault owns: created or edited through the Vault
10
+ * (`created_by` is set), or written by a release that marked it in the
11
+ * description. Rows an app registered for itself, and a person's own keys, are
12
+ * neither. Callers rule out `user` scope themselves.
13
+ */
14
+ export declare function isVaultManagedSecret(meta: {
15
+ description?: string | null | undefined;
16
+ createdBy?: string | null | undefined;
17
+ } | null | undefined): boolean;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Marker on `app_secrets.description` that releases before the Vault moved
3
+ * onto `app_secrets` wrote on every Vault-owned row. New Vault writes mark a
4
+ * row with `created_by` instead: such a release deletes marked rows that its
5
+ * own Vault table has no entry for, and it must never find a new key that way.
6
+ */
7
+ export const VAULT_SYNC_DESCRIPTION_PREFIX = "Synced from Dispatch vault:";
8
+ /**
9
+ * True for a key the Dispatch Vault owns: created or edited through the Vault
10
+ * (`created_by` is set), or written by a release that marked it in the
11
+ * description. Rows an app registered for itself, and a person's own keys, are
12
+ * neither. Callers rule out `user` scope themselves.
13
+ */
14
+ export function isVaultManagedSecret(meta) {
15
+ if (!meta)
16
+ return false;
17
+ return (!!meta.createdBy ||
18
+ !!meta.description?.startsWith(VAULT_SYNC_DESCRIPTION_PREFIX));
19
+ }
@@ -54,9 +54,9 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
54
54
  error: string;
55
55
  }>;
56
56
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
57
- errorCode?: undefined;
58
57
  code?: undefined;
59
58
  error: string;
59
+ errorCode?: undefined;
60
60
  } | {
61
61
  code?: undefined;
62
62
  error: string;
@@ -66,9 +66,9 @@ export declare function createAgentEngineApiKeyHandler(): import("h3").EventHand
66
66
  error: string;
67
67
  code: ProviderKeyCheckCode;
68
68
  } | {
69
- errorCode?: undefined;
70
69
  error?: undefined;
71
70
  code?: undefined;
71
+ errorCode?: undefined;
72
72
  ok: boolean;
73
73
  key: string;
74
74
  baseUrlKey?: string | undefined;
@@ -66,9 +66,10 @@ export declare function createBuilderSignupExperiment(dependencies: {
66
66
  status: "unavailable";
67
67
  code: string;
68
68
  } | {
69
- status: "disabled";
70
69
  code?: undefined;
70
+ status: "disabled";
71
71
  } | {
72
+ code?: undefined;
72
73
  id: string;
73
74
  variant: "automatic" | "control";
74
75
  issuedAt: number;
@@ -79,7 +80,6 @@ export declare function createBuilderSignupExperiment(dependencies: {
79
80
  policy: "builder-signup-2026-11-06";
80
81
  status: "assigned";
81
82
  experimentId: string;
82
- code?: undefined;
83
83
  } | {
84
84
  status: "disabled";
85
85
  code: string;
@@ -106,9 +106,10 @@ export declare function createBuilderSignupExperiment(dependencies: {
106
106
  status: "unavailable";
107
107
  code: string;
108
108
  } | {
109
- status: "disabled";
110
109
  code?: undefined;
110
+ status: "disabled";
111
111
  } | {
112
+ code?: undefined;
112
113
  id: string;
113
114
  variant: "automatic" | "control";
114
115
  issuedAt: number;
@@ -119,7 +120,6 @@ export declare function createBuilderSignupExperiment(dependencies: {
119
120
  policy: "builder-signup-2026-11-06";
120
121
  status: "assigned";
121
122
  experimentId: string;
122
- code?: undefined;
123
123
  } | {
124
124
  status: "disabled";
125
125
  code: string;
@@ -27,14 +27,14 @@ async function canReadDesignatedVaultFallback(vaultOrgId, credentialKey) {
27
27
  }
28
28
  try {
29
29
  const result = await getDbExec().execute({
30
+ // A grant names its key itself; legacy grants without `credential_key`
31
+ // match nothing until they are backfilled.
30
32
  sql: `SELECT 1
31
- FROM vault_grants AS grants
32
- INNER JOIN vault_secrets AS secrets ON secrets.id = grants.secret_id
33
- WHERE grants.org_id = ?
34
- AND grants.app_id = ?
35
- AND grants.status = 'active'
36
- AND secrets.org_id = grants.org_id
37
- AND secrets.credential_key = ?
33
+ FROM vault_grants
34
+ WHERE org_id = ?
35
+ AND app_id = ?
36
+ AND status = 'active'
37
+ AND credential_key = ?
38
38
  LIMIT 1`,
39
39
  args: [vaultOrgId, appId, credentialKey],
40
40
  });
@@ -17,8 +17,8 @@ export type FirstTouchSyncResult = {
17
17
  * browser's `an_ft` and `an_lt` again, so Safari keeps them their 30 days.
18
18
  */
19
19
  export declare function createFirstTouchHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
20
- error: string;
21
20
  reason?: undefined;
21
+ error: string;
22
22
  stored?: undefined;
23
23
  firstTouch?: undefined;
24
24
  } | {
@@ -17,8 +17,8 @@
17
17
  * typed JSON-in / JSON-out).
18
18
  */
19
19
  export declare function createTranscribeVoiceHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
20
- error: string;
21
20
  text?: undefined;
21
+ error: string;
22
22
  } | {
23
23
  error?: undefined;
24
24
  text: string;
@@ -120,6 +120,17 @@ resource CSP should include the exact origins of apps granted through Dispatch,
120
120
  not broad sources like `https:`. This lets Claude's transplant path fetch the
121
121
  signed target app HTML while keeping the connector's resource surface narrow.
122
122
 
123
+ Side panel and app tools: every `embedApp()` resource sets
124
+ `_meta.ui.preferredDisplayMode: "fullscreen"`, so Claude opens the app in its
125
+ side panel beside the chat (other hosts ignore the field). The shell declares
126
+ `availableDisplayModes` and `tools` in `ui/initialize` and answers the host's
127
+ `tools/list` / `tools/call` on the open view: `read_view` returns the page's
128
+ path, title, selection and visible text, `navigate_view` moves the open app to
129
+ another same-origin path in place, and any page tools the app registered through
130
+ WebMCP are passed through. Prefer these over a new `open_app` call when the user
131
+ asks to change what the open panel shows, since each new UI tool call opens a
132
+ new view.
133
+
123
134
  Host sizing rule: the MCP resource shell owns a bounded inline height and the
124
135
  embedded route should scroll internally. `embedApp({ height })` defaults to a
125
136
  `560px` shell, clamps to `320-900px`, and subtracts `44px` for the wrapper bar
@@ -322,7 +322,7 @@ pnpm action db-check-scoping --require-org # Also require org_id
322
322
  | OAuth tokens (Google, GitHub) | `oauth_tokens` store via `saveOAuthTokens()` |
323
323
  | Session tokens | Automatic (Better Auth handles this) |
324
324
 
325
- Per-user/per-org credentials and the vault are encrypted at rest with AES-256-GCM. The Dispatch vault keeps no plaintext copy: `vault_secrets` holds names and metadata, and each value lives only in `app_secrets`. For a per-user/per-org credential, the key precedence is `<APP_NAME>_SECRETS_ENCRYPTION_KEY` (app-scoped — useful in a multi-app workspace where apps shouldn't share one key) → `SECRETS_ENCRYPTION_KEY` → `BETTER_AUTH_SECRET`; production refuses to start without one of these set. The shared workspace vault (`app_secrets`, meant to decrypt the same in sibling apps) prefers `WORKSPACE_SECRETS_ENCRYPTION_KEY`, then the legacy combined `SECRETS_ENCRYPTION_KEY`, then material derived from `A2A_SECRET`, with `BETTER_AUTH_SECRET` and the app-scoped key retained as compatibility candidates. Successful fallback reads compare-and-swap re-encrypt the shared column under the preferred key. During a workspace-key rotation, set `WORKSPACE_SECRETS_ENCRYPTION_KEY_PREVIOUS` to the old value until reads have migrated existing rows. To encrypt any pre-existing plaintext credential rows in place, run `pnpm action db-migrate-encrypt-credentials` (idempotent, non-destructive).
325
+ Per-user/per-org credentials and the vault are encrypted at rest with AES-256-GCM. The Dispatch vault keeps no plaintext copy: each key's name, label, and encrypted value live together in its `app_secrets` row, and the vault's own tables hold only grants, requests, and the audit log. For a per-user/per-org credential, the key precedence is `<APP_NAME>_SECRETS_ENCRYPTION_KEY` (app-scoped — useful in a multi-app workspace where apps shouldn't share one key) → `SECRETS_ENCRYPTION_KEY` → `BETTER_AUTH_SECRET`; production refuses to start without one of these set. The shared workspace vault (`app_secrets`, meant to decrypt the same in sibling apps) prefers `WORKSPACE_SECRETS_ENCRYPTION_KEY`, then the legacy combined `SECRETS_ENCRYPTION_KEY`, then material derived from `A2A_SECRET`, with `BETTER_AUTH_SECRET` and the app-scoped key retained as compatibility candidates. Successful fallback reads compare-and-swap re-encrypt the shared column under the preferred key. During a workspace-key rotation, set `WORKSPACE_SECRETS_ENCRYPTION_KEY_PREVIOUS` to the old value until reads have migrated existing rows. To encrypt any pre-existing plaintext credential rows in place, run `pnpm action db-migrate-encrypt-credentials` (idempotent, non-destructive).
326
326
 
327
327
  Never store secrets in `settings`, `application_state`, source code, or action responses. Use the credential / vault APIs above — they handle both encryption and per-user scoping.
328
328