@flowdular/sdk 0.3.0 → 0.3.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.
Files changed (70) hide show
  1. package/.ai/platform-capabilities.md +2 -2
  2. package/.ai/policies/capabilities.yaml +30 -3
  3. package/.ai/references/catalog/module.json +1 -1
  4. package/.ai/references/catalog/package.json +2 -2
  5. package/.ai/references/catalog/spec/module.yaml +1 -1
  6. package/.ai/references/catalog.provenance.json +6 -6
  7. package/.ai/skills/cli-extension/SKILL.md +1 -1
  8. package/.ai/skills/deploy-operate/SKILL.md +7 -2
  9. package/modules/approvals/migrations/0005_approvals_grant_audit.down.sql +4 -0
  10. package/modules/approvals/migrations/0005_approvals_grant_audit.up.sql +20 -0
  11. package/modules/approvals/module.json +1 -1
  12. package/modules/approvals/package.json +1 -1
  13. package/modules/approvals/spec/module.yaml +11 -2
  14. package/modules/approvals/src/domain/capability.ts +12 -0
  15. package/modules/approvals/src/domain/grant.ts +69 -0
  16. package/modules/approvals/src/domain/types.ts +14 -0
  17. package/modules/approvals/src/index.ts +9 -0
  18. package/modules/approvals/src/platform.ts +8 -0
  19. package/modules/approvals/src/server/runtime.ts +4 -0
  20. package/modules/approvals/src/services/approvals-service.ts +80 -0
  21. package/modules/approvals/src/services/database-repository.ts +65 -6
  22. package/modules/approvals/src/services/migration.ts +34 -0
  23. package/modules/approvals/src/services/repository.ts +8 -0
  24. package/modules/connectors/migrations/0003_connectors_rotation_inventory.down.sql +2 -0
  25. package/modules/connectors/migrations/0003_connectors_rotation_inventory.up.sql +19 -0
  26. package/modules/connectors/module.json +7 -3
  27. package/modules/connectors/package.json +2 -1
  28. package/modules/connectors/spec/module.yaml +2 -1
  29. package/modules/connectors/src/cli/commands.json +17 -0
  30. package/modules/connectors/src/cli/index.ts +126 -0
  31. package/modules/connectors/src/services/credential-rotation.ts +221 -0
  32. package/modules/connectors/src/services/credential-vault.ts +6 -0
  33. package/modules/connectors/src/services/migration.ts +36 -0
  34. package/modules/documents/migrations/0003_documents_rotation_inventory.down.sql +2 -0
  35. package/modules/documents/migrations/0003_documents_rotation_inventory.up.sql +18 -0
  36. package/modules/documents/module.json +7 -3
  37. package/modules/documents/package.json +2 -1
  38. package/modules/documents/spec/module.yaml +2 -1
  39. package/modules/documents/src/cli/commands.json +17 -0
  40. package/modules/documents/src/cli/index.ts +145 -0
  41. package/modules/documents/src/services/database-repository.ts +15 -4
  42. package/modules/documents/src/services/documents-service.ts +13 -9
  43. package/modules/documents/src/services/migration.ts +35 -0
  44. package/modules/documents/src/services/repository.ts +12 -2
  45. package/modules/documents/src/services/storage-rotation.ts +157 -0
  46. package/modules/exports/migrations/0003_exports_rotation_inventory.down.sql +1 -0
  47. package/modules/exports/migrations/0003_exports_rotation_inventory.up.sql +9 -0
  48. package/modules/exports/module.json +7 -3
  49. package/modules/exports/package.json +2 -1
  50. package/modules/exports/spec/module.yaml +2 -1
  51. package/modules/exports/src/cli/commands.json +17 -0
  52. package/modules/exports/src/cli/index.ts +145 -0
  53. package/modules/exports/src/server/index.ts +0 -1
  54. package/modules/exports/src/services/data-classes.ts +16 -13
  55. package/modules/exports/src/services/database-repository.ts +30 -32
  56. package/modules/exports/src/services/migration.ts +27 -0
  57. package/modules/exports/src/services/repository.ts +9 -10
  58. package/modules/exports/src/services/storage-rotation.ts +138 -0
  59. package/package.json +1 -1
  60. package/packages/contracts/src/index.ts +1 -1
  61. package/packages/database/src/backup.ts +1 -0
  62. package/packages/database/src/migrations.ts +7 -0
  63. package/packages/harness/src/runtime.ts +169 -10
  64. package/packages/harness/src/tool-adapters.ts +6 -13
  65. package/packages/kernel/src/approval-grant.ts +310 -0
  66. package/packages/kernel/src/index.ts +20 -0
  67. package/packages/storage/src/envelope.ts +70 -21
  68. package/packages/storage/src/index.ts +7 -1
  69. package/packages/storage/src/port.ts +12 -1
  70. package/packages/storage/src/reseal.ts +128 -0
@@ -121,30 +121,30 @@ function describe(key: string, header: FrameHeader): StoredObject {
121
121
  };
122
122
  }
123
123
 
124
- export function encodeStoredObject(
124
+ interface FrameDraft {
125
+ readonly contentType: string;
126
+ readonly bytes: number;
127
+ readonly checksum: string;
128
+ readonly scan: StorageScanVerdict;
129
+ readonly createdAt: string;
130
+ }
131
+
132
+ function sealFrame(
125
133
  keyring: Keyring,
126
134
  key: string,
127
135
  plaintext: Uint8Array,
128
- metadata: {
129
- readonly contentType: string;
130
- readonly scan: StorageScanVerdict;
131
- readonly createdAt: Date;
132
- },
136
+ draft: FrameDraft,
133
137
  ): { readonly frame: Uint8Array; readonly object: StoredObject } {
134
- const draft = {
138
+ const unsealed = {
135
139
  v: 1,
136
140
  keyId: keyring.keyId,
137
141
  iv: '',
138
142
  tag: '',
139
- contentType: metadata.contentType,
140
- bytes: plaintext.byteLength,
141
- checksum: storageChecksum(plaintext),
142
- scan: metadata.scan,
143
- createdAt: metadata.createdAt.toISOString(),
143
+ ...draft,
144
144
  } satisfies FrameHeader;
145
- const sealed = keyring.seal(plaintext, additionalData(key, draft));
145
+ const sealed = keyring.seal(plaintext, additionalData(key, unsealed));
146
146
  const header: FrameHeader = {
147
- ...draft,
147
+ ...unsealed,
148
148
  keyId: sealed.keyId,
149
149
  iv: sealed.iv.toString('base64'),
150
150
  tag: sealed.tag.toString('base64'),
@@ -162,19 +162,30 @@ export function encodeStoredObject(
162
162
  };
163
163
  }
164
164
 
165
- /** Metadata only. The bytes are authenticated by `openStoredObject`, not here. */
166
- export function readStoredObjectHeader(
165
+ export function encodeStoredObject(
166
+ keyring: Keyring,
167
167
  key: string,
168
- frame: Uint8Array,
169
- ): StoredObject {
170
- return describe(key, parseHeader(frame).header);
168
+ plaintext: Uint8Array,
169
+ metadata: {
170
+ readonly contentType: string;
171
+ readonly scan: StorageScanVerdict;
172
+ readonly createdAt: Date;
173
+ },
174
+ ): { readonly frame: Uint8Array; readonly object: StoredObject } {
175
+ return sealFrame(keyring, key, plaintext, {
176
+ contentType: metadata.contentType,
177
+ bytes: plaintext.byteLength,
178
+ checksum: storageChecksum(plaintext),
179
+ scan: metadata.scan,
180
+ createdAt: metadata.createdAt.toISOString(),
181
+ });
171
182
  }
172
183
 
173
- export function openStoredObject(
184
+ function openFrame(
174
185
  keyring: Keyring,
175
186
  key: string,
176
187
  frame: Uint8Array,
177
- ): { readonly object: StoredObject; readonly plaintext: Buffer } {
188
+ ): { readonly header: FrameHeader; readonly plaintext: Buffer } {
178
189
  const { header, ciphertextOffset } = parseHeader(frame);
179
190
  const plaintext = keyring.open(
180
191
  {
@@ -185,5 +196,43 @@ export function openStoredObject(
185
196
  },
186
197
  additionalData(key, header),
187
198
  );
199
+ return { header, plaintext };
200
+ }
201
+
202
+ /** Metadata only. The bytes are authenticated by `openStoredObject`, not here. */
203
+ export function readStoredObjectHeader(
204
+ key: string,
205
+ frame: Uint8Array,
206
+ ): StoredObject {
207
+ return describe(key, parseHeader(frame).header);
208
+ }
209
+
210
+ export function openStoredObject(
211
+ keyring: Keyring,
212
+ key: string,
213
+ frame: Uint8Array,
214
+ ): { readonly object: StoredObject; readonly plaintext: Buffer } {
215
+ const { header, plaintext } = openFrame(keyring, key, frame);
188
216
  return { object: describe(key, header), plaintext };
189
217
  }
218
+
219
+ /**
220
+ * The same object under the current key. The frame is opened under the key it
221
+ * names, which authenticates the header, and the header's own fields become the
222
+ * additional data of the new frame, so nothing about the object changes but
223
+ * the key, the nonce, the tag and the ciphertext.
224
+ */
225
+ export function resealStoredObject(
226
+ keyring: Keyring,
227
+ key: string,
228
+ frame: Uint8Array,
229
+ ): { readonly frame: Uint8Array; readonly object: StoredObject } {
230
+ const { header, plaintext } = openFrame(keyring, key, frame);
231
+ return sealFrame(keyring, key, plaintext, {
232
+ contentType: header.contentType,
233
+ bytes: header.bytes,
234
+ checksum: header.checksum,
235
+ scan: header.scan,
236
+ createdAt: header.createdAt,
237
+ });
238
+ }
@@ -31,7 +31,13 @@ export { createLocalObjectStore } from './local.ts';
31
31
  export { createS3ObjectStore } from './s3.ts';
32
32
  export type { S3ObjectStoreOptions } from './s3.ts';
33
33
  export { createStoragePort } from './port.ts';
34
- export type { StoragePortOptions } from './port.ts';
34
+ export type { ManagedStoragePort, StoragePortOptions } from './port.ts';
35
+ export type {
36
+ StorageResealCount,
37
+ StorageResealOptions,
38
+ StorageResealPort,
39
+ StorageResealReport,
40
+ } from './reseal.ts';
35
41
  export {
36
42
  mintStorageReadToken,
37
43
  openStorageReadToken,
@@ -24,6 +24,7 @@ import {
24
24
  import { createLocalObjectStore } from './local.ts';
25
25
  import { createS3ObjectStore } from './s3.ts';
26
26
  import { mintStorageReadToken, storageReadUrl } from './read-token.ts';
27
+ import { createStorageResealer, type StorageResealPort } from './reseal.ts';
27
28
  import { unscannedStorageScanner, type StorageScanner } from './scanner.ts';
28
29
  import type { StorageConfig } from './config.ts';
29
30
  import type { ObjectStore } from './store.ts';
@@ -125,6 +126,9 @@ function objectStoreFor(
125
126
  });
126
127
  }
127
128
 
129
+ /** The port a deployment builds: the module contract plus the operator pass. */
130
+ export interface ManagedStoragePort extends StoragePort, StorageResealPort {}
131
+
128
132
  /**
129
133
  * The one place the storage rules live: the tenant-first key layout, the size
130
134
  * limit, the content type allowlist with magic byte verification, the scanner
@@ -133,11 +137,12 @@ function objectStoreFor(
133
137
  export function createStoragePort(
134
138
  config: StorageConfig,
135
139
  options: StoragePortOptions,
136
- ): StoragePort {
140
+ ): ManagedStoragePort {
137
141
  const store = objectStoreFor(config, options);
138
142
  const scanner = options.scanner ?? unscannedStorageScanner;
139
143
  const clock = options.clock ?? (() => new Date());
140
144
  const keyring = options.keyring;
145
+ const resealer = createStorageResealer(store, keyring);
141
146
  let disposed = false;
142
147
 
143
148
  const live = (): void => {
@@ -242,6 +247,12 @@ export function createStoragePort(
242
247
  return prefix ? readStoredObjectHeader(key, prefix) : null;
243
248
  },
244
249
 
250
+ keyId: keyring.keyId,
251
+ reseal(objects, options) {
252
+ live();
253
+ return resealer.reseal(objects, options);
254
+ },
255
+
245
256
  async dispose(): Promise<void> {
246
257
  if (disposed) return;
247
258
  disposed = true;
@@ -0,0 +1,128 @@
1
+ import { KeyringError, type Keyring } from '@flowdular/sdk/kernel';
2
+ import {
3
+ StorageError,
4
+ storageObjectKey,
5
+ type StorageObjectRef,
6
+ } from './contracts.ts';
7
+ import {
8
+ readStoredObjectHeader,
9
+ resealStoredObject,
10
+ STORAGE_HEADER_PREFIX_BYTES,
11
+ } from './envelope.ts';
12
+ import type { ObjectStore } from './store.ts';
13
+
14
+ export interface StorageResealCount {
15
+ readonly keyId: string;
16
+ readonly objects: number;
17
+ }
18
+
19
+ export interface StorageResealReport {
20
+ /** Key id every object should end on: the current key of the ring. */
21
+ readonly currentKeyId: string;
22
+ readonly counts: readonly StorageResealCount[];
23
+ /** Objects on a retired key the ring still holds. */
24
+ readonly stale: number;
25
+ readonly resealed: number;
26
+ /** Objects sealed under a key id this ring does not hold; left as they are. */
27
+ readonly unknown: number;
28
+ /** Objects that failed authentication under the key they name; left as they are. */
29
+ readonly refused: number;
30
+ /** References with no object behind them. */
31
+ readonly missing: number;
32
+ }
33
+
34
+ export interface StorageResealOptions {
35
+ readonly apply: boolean;
36
+ }
37
+
38
+ export interface StorageResealPort {
39
+ /** Key id every re-seal writes: the current key of the ring. */
40
+ readonly keyId: string;
41
+ /**
42
+ * Re-seals every object of the batch that a retired key sealed, in place.
43
+ * The caller owns the inventory: an adapter cannot list, so the references
44
+ * come from the rows that name the objects. Without `apply` the headers are
45
+ * read and counted and nothing is written.
46
+ */
47
+ reseal(
48
+ objects: readonly StorageObjectRef[],
49
+ options: StorageResealOptions,
50
+ ): Promise<StorageResealReport>;
51
+ }
52
+
53
+ /* A frame the port cannot parse is left exactly as the one it cannot open:
54
+ the pass counts it and moves on, and the operator restores it. */
55
+ function corrupt(error: unknown): boolean {
56
+ return error instanceof StorageError && error.code === 'OBJECT_CORRUPT';
57
+ }
58
+
59
+ export function createStorageResealer(
60
+ store: ObjectStore,
61
+ keyring: Keyring,
62
+ ): StorageResealPort {
63
+ return {
64
+ keyId: keyring.keyId,
65
+ async reseal(objects, options) {
66
+ const counts = new Map<string, number>();
67
+ let stale = 0;
68
+ let resealed = 0;
69
+ let unknown = 0;
70
+ let refused = 0;
71
+ let missing = 0;
72
+ for (const reference of objects) {
73
+ const key = storageObjectKey(reference);
74
+ const prefix = await store.read(key, STORAGE_HEADER_PREFIX_BYTES);
75
+ if (!prefix) {
76
+ missing += 1;
77
+ continue;
78
+ }
79
+ let keyId: string;
80
+ try {
81
+ keyId = readStoredObjectHeader(key, prefix).keyId;
82
+ } catch (error) {
83
+ if (corrupt(error)) {
84
+ refused += 1;
85
+ continue;
86
+ }
87
+ throw error;
88
+ }
89
+ counts.set(keyId, (counts.get(keyId) ?? 0) + 1);
90
+ if (keyId === keyring.keyId) continue;
91
+ if (!keyring.knows(keyId)) {
92
+ unknown += 1;
93
+ continue;
94
+ }
95
+ stale += 1;
96
+ if (!options.apply) continue;
97
+ const frame = await store.read(key);
98
+ if (!frame) {
99
+ missing += 1;
100
+ continue;
101
+ }
102
+ let next;
103
+ try {
104
+ next = resealStoredObject(keyring, key, frame);
105
+ } catch (error) {
106
+ if (error instanceof KeyringError || corrupt(error)) {
107
+ refused += 1;
108
+ continue;
109
+ }
110
+ throw error;
111
+ }
112
+ await store.write(key, next.frame);
113
+ resealed += 1;
114
+ }
115
+ return {
116
+ currentKeyId: keyring.keyId,
117
+ counts: [...counts]
118
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
119
+ .map(([keyId, count]) => ({ keyId, objects: count })),
120
+ stale,
121
+ resealed,
122
+ unknown,
123
+ refused,
124
+ missing,
125
+ };
126
+ },
127
+ };
128
+ }