@sanoma-ai/workflows 0.9.1 → 0.10.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/README.md +193 -50
- package/SECRET-SUPPORT.md +33 -0
- package/dist/apply-steps.js +5 -2
- package/dist/apply-steps.js.map +1 -1
- package/dist/apply.d.ts +3 -0
- package/dist/apply.js +27 -13
- package/dist/apply.js.map +1 -1
- package/dist/approvals.d.ts +1 -0
- package/dist/approvals.js +6 -1
- package/dist/approvals.js.map +1 -1
- package/dist/apps.d.ts +13 -24
- package/dist/apps.js +69 -68
- package/dist/apps.js.map +1 -1
- package/dist/ast.d.ts +9 -7
- package/dist/ast.js +2 -0
- package/dist/ast.js.map +1 -1
- package/dist/authentication.d.ts +54 -0
- package/dist/authentication.js +138 -0
- package/dist/authentication.js.map +1 -0
- package/dist/call-workflow.d.ts +5 -0
- package/dist/call-workflow.js +4 -3
- package/dist/call-workflow.js.map +1 -1
- package/dist/call.d.ts +3 -2
- package/dist/call.js +36 -7
- package/dist/call.js.map +1 -1
- package/dist/cedar.js +14 -1
- package/dist/cedar.js.map +1 -1
- package/dist/change-steps.js +7 -5
- package/dist/change-steps.js.map +1 -1
- package/dist/change.d.ts +7 -0
- package/dist/change.js +7 -0
- package/dist/change.js.map +1 -1
- package/dist/client.d.ts +62 -10
- package/dist/client.js +272 -47
- package/dist/client.js.map +1 -1
- package/dist/collection.d.ts +7 -0
- package/dist/collection.js +5 -2
- package/dist/collection.js.map +1 -1
- package/dist/config.d.ts +16 -4
- package/dist/config.js +89 -21
- package/dist/config.js.map +1 -1
- package/dist/connection.d.ts +389 -0
- package/dist/connection.js +657 -0
- package/dist/connection.js.map +1 -0
- package/dist/credentials.d.ts +7 -53
- package/dist/credentials.js +84 -247
- package/dist/credentials.js.map +1 -1
- package/dist/db.js +34 -2
- package/dist/db.js.map +1 -1
- package/dist/define.d.ts +9 -2
- package/dist/define.js.map +1 -1
- package/dist/definitions.d.ts +20 -0
- package/dist/definitions.js +16 -0
- package/dist/definitions.js.map +1 -0
- package/dist/deployment-key.d.ts +2 -0
- package/dist/deployment-key.js +22 -0
- package/dist/deployment-key.js.map +1 -0
- package/dist/describe.d.ts +15 -175
- package/dist/describe.js +122 -35
- package/dist/describe.js.map +1 -1
- package/dist/description.d.ts +532 -0
- package/dist/description.js +381 -0
- package/dist/description.js.map +1 -0
- package/dist/direct.js +7 -0
- package/dist/direct.js.map +1 -1
- package/dist/drift.d.ts +1 -1
- package/dist/drift.js +1 -1
- package/dist/drift.js.map +1 -1
- package/dist/env.d.ts +2 -0
- package/dist/env.js +2 -2
- package/dist/env.js.map +1 -1
- package/dist/fake.d.ts +13 -1
- package/dist/fake.js +70 -0
- package/dist/fake.js.map +1 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/ledger.d.ts +2 -0
- package/dist/ledger.js +1 -1
- package/dist/ledger.js.map +1 -1
- package/dist/lint.d.ts +0 -9
- package/dist/lint.js +2 -17
- package/dist/lint.js.map +1 -1
- package/dist/load.d.ts +85 -0
- package/dist/load.js +391 -0
- package/dist/load.js.map +1 -0
- package/dist/lockfile.d.ts +2 -0
- package/dist/lockfile.js +8 -3
- package/dist/lockfile.js.map +1 -1
- package/dist/op.d.ts +22 -5
- package/dist/op.js +20 -2
- package/dist/op.js.map +1 -1
- package/dist/outline.d.ts +5 -0
- package/dist/outline.js +46 -5
- package/dist/outline.js.map +1 -1
- package/dist/paths.d.ts +18 -2
- package/dist/paths.js +19 -7
- package/dist/paths.js.map +1 -1
- package/dist/plugin.d.ts +0 -11
- package/dist/plugin.js +3 -18
- package/dist/plugin.js.map +1 -1
- package/dist/policy.d.ts +2 -0
- package/dist/policy.js.map +1 -1
- package/dist/resource.d.ts +10 -4
- package/dist/resource.js +11 -4
- package/dist/resource.js.map +1 -1
- package/dist/resources.d.ts +33 -76
- package/dist/resources.js +219 -227
- package/dist/resources.js.map +1 -1
- package/dist/run.d.ts +4 -0
- package/dist/run.js.map +1 -1
- package/dist/sandbox.d.ts +5 -2
- package/dist/sandbox.js +56 -7
- package/dist/sandbox.js.map +1 -1
- package/dist/scenario.d.ts +5 -0
- package/dist/scenario.js +3 -2
- package/dist/scenario.js.map +1 -1
- package/dist/scenarios.js +7 -1
- package/dist/scenarios.js.map +1 -1
- package/dist/schema.js +8 -0
- package/dist/schema.js.map +1 -1
- package/dist/secret-store-check.d.ts +11 -0
- package/dist/secret-store-check.js +76 -0
- package/dist/secret-store-check.js.map +1 -0
- package/dist/secret-store.d.ts +106 -0
- package/dist/secret-store.js +228 -0
- package/dist/secret-store.js.map +1 -0
- package/dist/secret.d.ts +2 -0
- package/dist/secret.js +32 -0
- package/dist/secret.js.map +1 -1
- package/dist/shared.d.ts +21 -11
- package/dist/shared.js +41 -7
- package/dist/shared.js.map +1 -1
- package/dist/sites.d.ts +4 -93
- package/dist/sites.js +76 -84
- package/dist/sites.js.map +1 -1
- package/dist/steps.d.ts +3 -0
- package/dist/steps.js.map +1 -1
- package/dist/version.d.ts +2 -1
- package/dist/version.js +2 -1
- package/dist/version.js.map +1 -1
- package/dist/wakeup-driver.js +15 -6
- package/dist/wakeup-driver.js.map +1 -1
- package/dist/worker.js +12 -8
- package/dist/worker.js.map +1 -1
- package/dist/writer.d.ts +53 -14
- package/dist/writer.js +113 -43
- package/dist/writer.js.map +1 -1
- package/oxlint.json +1 -5
- package/package.json +6 -1
- package/src/apply-steps.ts +5 -2
- package/src/apply.ts +54 -21
- package/src/approvals.ts +7 -1
- package/src/apps.ts +71 -86
- package/src/ast.ts +6 -6
- package/src/authentication.ts +191 -0
- package/src/call-workflow.ts +5 -4
- package/src/call.ts +52 -9
- package/src/cedar.ts +14 -1
- package/src/change-steps.ts +7 -4
- package/src/change.ts +10 -0
- package/src/client.ts +346 -45
- package/src/collection.ts +10 -2
- package/src/config.ts +107 -19
- package/src/connection.ts +912 -0
- package/src/credentials.ts +94 -281
- package/src/db.ts +34 -2
- package/src/define.ts +7 -2
- package/src/definitions.ts +31 -0
- package/src/deployment-key.ts +23 -0
- package/src/describe.ts +150 -227
- package/src/description.ts +426 -0
- package/src/direct.ts +7 -0
- package/src/drift.ts +10 -2
- package/src/env.ts +4 -2
- package/src/fake.ts +71 -0
- package/src/index.ts +53 -1
- package/src/ledger.ts +3 -1
- package/src/lint.ts +2 -17
- package/src/load.ts +466 -0
- package/src/lockfile.ts +10 -3
- package/src/op.ts +41 -7
- package/src/outline.ts +53 -7
- package/src/paths.ts +21 -6
- package/src/plugin.ts +3 -24
- package/src/policy.ts +2 -0
- package/src/resource.ts +24 -6
- package/src/resources.ts +246 -301
- package/src/run.ts +4 -0
- package/src/sandbox.ts +61 -7
- package/src/scenario.ts +24 -6
- package/src/scenarios.ts +7 -1
- package/src/schema.ts +10 -0
- package/src/secret-store-check.ts +93 -0
- package/src/secret-store.ts +314 -0
- package/src/secret.ts +29 -0
- package/src/shared.ts +44 -10
- package/src/sites.ts +85 -179
- package/src/steps.ts +3 -2
- package/src/version.ts +2 -1
- package/src/wakeup-driver.ts +22 -6
- package/src/worker.ts +12 -8
- package/src/writer.ts +145 -56
- package/dist/datafile.d.ts +0 -97
- package/dist/datafile.js +0 -479
- package/dist/datafile.js.map +0 -1
- package/src/datafile.ts +0 -601
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { checkAuthenticationStore, decodeCredentials, defineAuthentication, verifyAuthentication, AuthenticationError, authenticationProblem, } from "./authentication.js";
|
|
4
|
+
import { setupOf, VENDOR, defineConnector, defineDriver } from "./op.js";
|
|
5
|
+
import { isSecret } from "./secret.js";
|
|
6
|
+
import { SanomaError } from "./errors.js";
|
|
7
|
+
import { SecretStoreError } from "./secret-store.js";
|
|
8
|
+
const idPart = /^[a-z][a-z0-9-]{0,63}$/;
|
|
9
|
+
function refuse(message) {
|
|
10
|
+
throw new SanomaError("invalid_input", message);
|
|
11
|
+
}
|
|
12
|
+
export const connectionRefSchema = z.object({
|
|
13
|
+
$connection: z.string().min(1),
|
|
14
|
+
vendor: z.string().min(1),
|
|
15
|
+
revision: z.string().optional(),
|
|
16
|
+
});
|
|
17
|
+
const snapshotSchema = z.object({
|
|
18
|
+
id: z.string(),
|
|
19
|
+
vendor: z.string(),
|
|
20
|
+
method: z.string(),
|
|
21
|
+
account: z.string(),
|
|
22
|
+
scopes: z.array(z.string()),
|
|
23
|
+
revision: z.string(),
|
|
24
|
+
definition: z.string(),
|
|
25
|
+
credentials: z.object({ $secret: z.string() }).optional(),
|
|
26
|
+
});
|
|
27
|
+
export const connectionTargetSchema = z.union([
|
|
28
|
+
z.object({ connection: connectionRefSchema }),
|
|
29
|
+
z.object({ rule: z.string().min(1) }),
|
|
30
|
+
]);
|
|
31
|
+
const setupSchema = z.object({
|
|
32
|
+
target: connectionTargetSchema,
|
|
33
|
+
id: z.string(),
|
|
34
|
+
vendor: z.string(),
|
|
35
|
+
method: z.string(),
|
|
36
|
+
definition: z.string(),
|
|
37
|
+
credential: z.string().optional(),
|
|
38
|
+
location: z
|
|
39
|
+
.object({ store: z.string(), namespace: z.string(), path: z.string(), management: z.enum(["sanoma", "external"]) })
|
|
40
|
+
.optional(),
|
|
41
|
+
expectedRevision: z.string().nullable(),
|
|
42
|
+
requiredScopes: z.array(z.string()),
|
|
43
|
+
expectedAccount: z.string().optional(),
|
|
44
|
+
previousAccount: z.string().optional(),
|
|
45
|
+
previousLocation: z
|
|
46
|
+
.object({ store: z.string(), namespace: z.string(), path: z.string(), management: z.enum(["sanoma", "external"]) })
|
|
47
|
+
.optional(),
|
|
48
|
+
});
|
|
49
|
+
export function defineConnection(connector, options) {
|
|
50
|
+
if (!idPart.test(options.name))
|
|
51
|
+
refuse("A connection needs a stable lowercase name");
|
|
52
|
+
const vendor = connector[VENDOR].id;
|
|
53
|
+
const auth = setupOf(connector).authentication?.[options.method];
|
|
54
|
+
if (!auth)
|
|
55
|
+
refuse("The connector does not declare this authentication method");
|
|
56
|
+
if (auth.kind === "workload-identity" ? options.credentials !== undefined : !isSecret(options.credentials))
|
|
57
|
+
refuse("Credentials must match the authentication method");
|
|
58
|
+
return Object.freeze({
|
|
59
|
+
$connection: `${vendor}/${options.name}`,
|
|
60
|
+
vendor,
|
|
61
|
+
name: options.name,
|
|
62
|
+
method: options.method,
|
|
63
|
+
...(options.credentials && { credentials: Object.freeze({ $secret: options.credentials.$secret }) }),
|
|
64
|
+
scopes: Object.freeze([...(options.scopes ?? [])]),
|
|
65
|
+
...(options.account && { account: options.account }),
|
|
66
|
+
default: options.default ?? false,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export function defineConnectionRule(connector, options) {
|
|
70
|
+
if (!idPart.test(options.name) ||
|
|
71
|
+
!options.createBy.length ||
|
|
72
|
+
options.createBy.some((x) => typeof x === "string" ? !x.trim() : !x || typeof x.group !== "string" || !x.group.trim()))
|
|
73
|
+
refuse("A connection rule needs a name and allowed creators");
|
|
74
|
+
if (!setupOf(connector).authentication?.[options.method])
|
|
75
|
+
refuse("The connector does not declare this authentication method");
|
|
76
|
+
return Object.freeze({
|
|
77
|
+
name: options.name,
|
|
78
|
+
vendor: connector[VENDOR].id,
|
|
79
|
+
method: options.method,
|
|
80
|
+
createBy: Object.freeze(options.createBy.map((x) => (typeof x === "string" ? x : Object.freeze({ ...x })))),
|
|
81
|
+
scopes: Object.freeze([...(options.scopes ?? [])]),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/** Reconstitutes a dynamic reference; authority is checked from the persisted record on every call. */
|
|
85
|
+
export function connectionRef(rule, id) {
|
|
86
|
+
if (!id.startsWith(`${rule.vendor}/${rule.name}/`))
|
|
87
|
+
refuse("The connection is outside this rule");
|
|
88
|
+
return Object.freeze({ $connection: id, vendor: rule.vendor });
|
|
89
|
+
}
|
|
90
|
+
export function connectionCatalog(connectors, definitions = [], rules = []) {
|
|
91
|
+
const methods = new Map();
|
|
92
|
+
for (const connector of connectors) {
|
|
93
|
+
const vendor = connector[VENDOR].id;
|
|
94
|
+
const entries = methods.get(vendor) ?? {};
|
|
95
|
+
for (const [name, method] of Object.entries(setupOf(connector).authentication ?? {})) {
|
|
96
|
+
if (entries[name] && entries[name] !== method)
|
|
97
|
+
refuse("Conflicting authentication methods for a connector");
|
|
98
|
+
defineAuthentication(method);
|
|
99
|
+
entries[name] = method;
|
|
100
|
+
}
|
|
101
|
+
methods.set(vendor, entries);
|
|
102
|
+
}
|
|
103
|
+
const defined = new Map();
|
|
104
|
+
const defaults = new Set();
|
|
105
|
+
for (const d of definitions) {
|
|
106
|
+
if (!d ||
|
|
107
|
+
!idPart.test(d.name) ||
|
|
108
|
+
d.$connection !== `${d.vendor}/${d.name}` ||
|
|
109
|
+
!methods.get(d.vendor)?.[d.method] ||
|
|
110
|
+
defined.has(d.$connection))
|
|
111
|
+
refuse("Invalid or duplicate connection declaration");
|
|
112
|
+
const auth = methods.get(d.vendor)[d.method];
|
|
113
|
+
if (auth.kind === "workload-identity" ? d.credentials !== undefined : !isSecret(d.credentials))
|
|
114
|
+
refuse("Credentials must match the authentication method");
|
|
115
|
+
if (d.default && defaults.has(d.vendor))
|
|
116
|
+
refuse("A connector can have only one explicit default connection");
|
|
117
|
+
if (d.default)
|
|
118
|
+
defaults.add(d.vendor);
|
|
119
|
+
defined.set(d.$connection, d);
|
|
120
|
+
}
|
|
121
|
+
const dynamic = new Map();
|
|
122
|
+
for (const r of rules) {
|
|
123
|
+
const id = `${r.vendor}/${r.name}`;
|
|
124
|
+
if (!idPart.test(r.name) ||
|
|
125
|
+
!methods.get(r.vendor)?.[r.method] ||
|
|
126
|
+
dynamic.has(id) ||
|
|
127
|
+
defined.has(id) ||
|
|
128
|
+
!r.createBy.length)
|
|
129
|
+
refuse("Invalid or duplicate connection rule");
|
|
130
|
+
dynamic.set(id, r);
|
|
131
|
+
}
|
|
132
|
+
return { definitions: defined, rules: dynamic, methods };
|
|
133
|
+
}
|
|
134
|
+
const snapshot = (r) => ({
|
|
135
|
+
id: r.id,
|
|
136
|
+
vendor: r.vendor,
|
|
137
|
+
method: r.method,
|
|
138
|
+
account: r.account,
|
|
139
|
+
scopes: r.scopes,
|
|
140
|
+
revision: r.revision,
|
|
141
|
+
definition: r.definition,
|
|
142
|
+
...(r.credential && { credentials: { $secret: r.credential } }),
|
|
143
|
+
});
|
|
144
|
+
const fingerprint = (declaration, method) => createHash("sha256")
|
|
145
|
+
.update(JSON.stringify({ declaration, kind: method.kind, version: method.version, storage: method.storage }))
|
|
146
|
+
.digest("hex");
|
|
147
|
+
/** Persists account metadata only. Secret values are handled outside these operations. */
|
|
148
|
+
export class ConnectionRegistry {
|
|
149
|
+
catalog;
|
|
150
|
+
db;
|
|
151
|
+
secrets;
|
|
152
|
+
constructor(catalog, db, secrets) {
|
|
153
|
+
this.catalog = catalog;
|
|
154
|
+
this.db = db;
|
|
155
|
+
this.secrets = secrets;
|
|
156
|
+
}
|
|
157
|
+
/** A direct metadata write uses the same transaction as its default ledger record. */
|
|
158
|
+
onDatabase(db) {
|
|
159
|
+
return new ConnectionRegistry(this.catalog, async () => db, this.secrets);
|
|
160
|
+
}
|
|
161
|
+
/** Safe card data only. Listing does not read credential values or contact a vendor. */
|
|
162
|
+
async cards(actor) {
|
|
163
|
+
const rows = (await (await this.db()).query("SELECT c.*, h.health, h.checked_at, h.externally_renewed FROM sanoma_connections c LEFT JOIN sanoma_connection_health h ON c.id=h.connection_id AND c.revision=h.revision WHERE c.owner IS NULL OR c.owner=$1", [actor])).rows;
|
|
164
|
+
const found = new Map(rows.map((r) => [r.id, r]));
|
|
165
|
+
const ids = new Set([...this.catalog.definitions.keys(), ...rows.filter((r) => r.rule).map((r) => r.id)]);
|
|
166
|
+
const cards = [];
|
|
167
|
+
for (const id of ids) {
|
|
168
|
+
const row = found.get(id);
|
|
169
|
+
const declaration = row?.rule ? this.catalog.rules.get(row.rule) : this.catalog.definitions.get(id);
|
|
170
|
+
if (!declaration || (row?.rule && row.owner !== actor))
|
|
171
|
+
continue;
|
|
172
|
+
const method = this.catalog.methods.get(declaration.vendor)?.[declaration.method];
|
|
173
|
+
if (!method)
|
|
174
|
+
continue;
|
|
175
|
+
const credential = method.kind === "workload-identity"
|
|
176
|
+
? undefined
|
|
177
|
+
: "default" in declaration
|
|
178
|
+
? declaration.credentials?.$secret
|
|
179
|
+
: row?.credential;
|
|
180
|
+
cards.push({
|
|
181
|
+
id,
|
|
182
|
+
vendor: declaration.vendor,
|
|
183
|
+
name: row?.rule ? id.slice(declaration.vendor.length + 1) : declaration.name,
|
|
184
|
+
method: declaration.method,
|
|
185
|
+
...(row && { account: row.account, revision: row.revision }),
|
|
186
|
+
scopes: row?.scopes ?? [],
|
|
187
|
+
requiredScopes: declaration.scopes,
|
|
188
|
+
...("account" in declaration && declaration.account && { expectedAccount: declaration.account }),
|
|
189
|
+
...(row?.owner && { owner: row.owner }),
|
|
190
|
+
status: !row
|
|
191
|
+
? "missing"
|
|
192
|
+
: row.definition !== this.bindingFingerprint(declaration, method, credential)
|
|
193
|
+
? "configuration_changed"
|
|
194
|
+
: row.status,
|
|
195
|
+
...(credential && this.secrets && { location: this.secrets.location(credential) }),
|
|
196
|
+
...(row?.location && { boundLocation: row.location }),
|
|
197
|
+
...(row?.updated_by && { updatedBy: row.updated_by }),
|
|
198
|
+
...(row?.updated_at && { updatedAt: row.updated_at.toISOString() }),
|
|
199
|
+
...(row?.health &&
|
|
200
|
+
row.checked_at && {
|
|
201
|
+
health: {
|
|
202
|
+
state: row.health,
|
|
203
|
+
checkedAt: row.checked_at.toISOString(),
|
|
204
|
+
externallyRenewed: row.externally_renewed ?? false,
|
|
205
|
+
},
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return cards;
|
|
210
|
+
}
|
|
211
|
+
/** Resolve code-defined setup authority without reading a credential. The request id stays
|
|
212
|
+
* stable across preview, approval and retry; account allocation is scoped to its principal. */
|
|
213
|
+
async setup(target, actor, requestId) {
|
|
214
|
+
let declaration;
|
|
215
|
+
let row;
|
|
216
|
+
let id;
|
|
217
|
+
let credential;
|
|
218
|
+
if ("rule" in target) {
|
|
219
|
+
const rule = this.catalog.rules.get(target.rule);
|
|
220
|
+
if (!rule ||
|
|
221
|
+
!rule.createBy.some((by) => (typeof by === "string" ? by === actor.id : actor.groups?.includes(by.group))))
|
|
222
|
+
throw new SanomaError("policy_denied", "This principal cannot create connections under this rule");
|
|
223
|
+
declaration = rule;
|
|
224
|
+
const allocation = createHash("sha256")
|
|
225
|
+
.update(JSON.stringify([actor.id, target.rule, requestId]))
|
|
226
|
+
.digest("hex");
|
|
227
|
+
id = `${target.rule}/${allocation}`;
|
|
228
|
+
credential = `CONNECTION_${allocation.toUpperCase()}`;
|
|
229
|
+
row = await this.row(id);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
id = target.connection.$connection;
|
|
233
|
+
row = await this.row(id);
|
|
234
|
+
declaration = row?.rule ? this.catalog.rules.get(row.rule) : this.catalog.definitions.get(id);
|
|
235
|
+
if (!declaration || declaration.vendor !== target.connection.vendor)
|
|
236
|
+
refuse("The connection is not declared");
|
|
237
|
+
if (row?.rule && row.owner !== actor.id)
|
|
238
|
+
throw new SanomaError("policy_denied", "The connection belongs to another principal");
|
|
239
|
+
credential = "credentials" in declaration ? declaration.credentials?.$secret : (row?.credential ?? undefined);
|
|
240
|
+
}
|
|
241
|
+
const method = this.catalog.methods.get(declaration.vendor)?.[declaration.method];
|
|
242
|
+
if (!method?.verify)
|
|
243
|
+
throw new AuthenticationError("unsupported");
|
|
244
|
+
if (method.kind === "workload-identity")
|
|
245
|
+
credential = undefined;
|
|
246
|
+
this.checkStore(method, credential);
|
|
247
|
+
return {
|
|
248
|
+
target,
|
|
249
|
+
id,
|
|
250
|
+
vendor: declaration.vendor,
|
|
251
|
+
method: declaration.method,
|
|
252
|
+
definition: this.bindingFingerprint(declaration, method, credential),
|
|
253
|
+
...(credential && { credential, ...(this.secrets && { location: this.secrets.location(credential) }) }),
|
|
254
|
+
expectedRevision: row?.revision ?? null,
|
|
255
|
+
requiredScopes: declaration.scopes,
|
|
256
|
+
...("account" in declaration && declaration.account && { expectedAccount: declaration.account }),
|
|
257
|
+
...(row && { previousAccount: row.account }),
|
|
258
|
+
...(row?.location && { previousLocation: row.location }),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/** Metadata-only, policy-authorized reservation. Calls cannot use it until activation verifies
|
|
262
|
+
* the saved bundle. A store write failure leaves an explicit uncertain, unusable account. */
|
|
263
|
+
async configure(target, requestId, expectedRevision, definition, account, scopes, actor) {
|
|
264
|
+
const setup = await this.setup(target, actor, requestId);
|
|
265
|
+
if (setup.definition !== definition || setup.expectedRevision !== expectedRevision)
|
|
266
|
+
refuse("The connection changed; review it again before saving");
|
|
267
|
+
if (setup.expectedAccount && setup.expectedAccount !== account)
|
|
268
|
+
throw new AuthenticationError("wrong_account");
|
|
269
|
+
if (setup.requiredScopes.some((s) => !scopes.includes(s)))
|
|
270
|
+
throw new AuthenticationError("scope_changed");
|
|
271
|
+
const current = await this.row(setup.id);
|
|
272
|
+
const row = {
|
|
273
|
+
id: setup.id,
|
|
274
|
+
vendor: setup.vendor,
|
|
275
|
+
method: setup.method,
|
|
276
|
+
owner: "rule" in target ? actor.id : (current?.owner ?? null),
|
|
277
|
+
rule: "rule" in target ? target.rule : (current?.rule ?? null),
|
|
278
|
+
account,
|
|
279
|
+
scopes,
|
|
280
|
+
revision: randomUUID(),
|
|
281
|
+
definition,
|
|
282
|
+
credential: setup.credential ?? null,
|
|
283
|
+
status: "pending",
|
|
284
|
+
};
|
|
285
|
+
await this.save(row, expectedRevision, actor.id);
|
|
286
|
+
return snapshot(row);
|
|
287
|
+
}
|
|
288
|
+
async uncertain(ref, revision, actor) {
|
|
289
|
+
await (await this.db()).query("UPDATE sanoma_connections SET status='uncertain',updated_by=$3,updated_at=now() WHERE id=$1 AND revision=$2 AND status='pending'", [ref.$connection, revision, actor]);
|
|
290
|
+
}
|
|
291
|
+
async storedVersion(id, revision) {
|
|
292
|
+
return (await (await this.db()).query("SELECT credential_version FROM sanoma_connection_setups WHERE connection_id=$1 AND revision=$2", [id, revision])).rows[0]?.credential_version;
|
|
293
|
+
}
|
|
294
|
+
async recordStoredVersion(id, revision, version) {
|
|
295
|
+
await (await this.db()).query("INSERT INTO sanoma_connection_setups (connection_id,revision,credential_version) SELECT id,revision,$3 FROM sanoma_connections WHERE id=$1 AND revision=$2 AND status='pending' ON CONFLICT DO NOTHING", [id, revision, version]);
|
|
296
|
+
if ((await this.storedVersion(id, revision)) !== version)
|
|
297
|
+
throw new SecretStoreError("conflict");
|
|
298
|
+
}
|
|
299
|
+
async health(id, revision, state, version, external = false) {
|
|
300
|
+
const previous = (await (await this.db()).query("SELECT credential_version FROM sanoma_connection_health WHERE connection_id=$1 AND revision=$2", [id, revision])).rows[0];
|
|
301
|
+
const renewed = external &&
|
|
302
|
+
previous?.credential_version != null &&
|
|
303
|
+
version !== undefined &&
|
|
304
|
+
previous.credential_version !== version;
|
|
305
|
+
const result = await (await this.db()).query("INSERT INTO sanoma_connection_health (connection_id,revision,health,credential_version,externally_renewed) SELECT id,revision,$3,$4,$5 FROM sanoma_connections WHERE id=$1 AND revision=$2 ON CONFLICT(connection_id) DO UPDATE SET revision=excluded.revision,health=excluded.health,credential_version=excluded.credential_version,externally_renewed=excluded.externally_renewed,checked_at=now() RETURNING checked_at", [id, revision, state, version ?? previous?.credential_version ?? null, renewed]);
|
|
306
|
+
const checked = result.rows[0];
|
|
307
|
+
if (!checked)
|
|
308
|
+
refuse("The connection changed during the test; reload before testing it again");
|
|
309
|
+
return { state, checkedAt: checked.checked_at.toISOString(), externallyRenewed: renewed };
|
|
310
|
+
}
|
|
311
|
+
async test(ref, actor) {
|
|
312
|
+
const setup = await this.setup({ connection: ref }, actor, "test");
|
|
313
|
+
const row = await this.row(setup.id);
|
|
314
|
+
if (!row)
|
|
315
|
+
return { state: "missing", checkedAt: new Date().toISOString(), externallyRenewed: false };
|
|
316
|
+
if (row.definition !== setup.definition)
|
|
317
|
+
return { state: "configuration_changed", checkedAt: new Date().toISOString(), externallyRenewed: false };
|
|
318
|
+
if (row.status !== "connected")
|
|
319
|
+
return this.health(row.id, row.revision, row.status);
|
|
320
|
+
try {
|
|
321
|
+
const material = await this.authenticate(snapshot(row), actor.id);
|
|
322
|
+
return this.health(row.id, row.revision, "connected", material.credentialVersion, setup.location?.management === "external" || !this.secrets?.store.capabilities.writable);
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
const code = error?.code;
|
|
326
|
+
const state = authenticationProblem(error) ??
|
|
327
|
+
(code === "denied" ? "store_denied" : code === "invalid_value" ? "invalid" : "unavailable");
|
|
328
|
+
return this.health(row.id, row.revision, state);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async setupState(selected, actor) {
|
|
332
|
+
const setup = await this.setup({ connection: { vendor: selected.vendor, $connection: selected.id } }, actor, "state");
|
|
333
|
+
const row = await this.row(selected.id);
|
|
334
|
+
if (!row ||
|
|
335
|
+
row.definition !== setup.definition ||
|
|
336
|
+
row.account !== selected.account ||
|
|
337
|
+
JSON.stringify(row.scopes.toSorted()) !== JSON.stringify(selected.scopes.toSorted()))
|
|
338
|
+
refuse("The connection changed; review it again before saving");
|
|
339
|
+
if (row.status === "connected")
|
|
340
|
+
return "connected";
|
|
341
|
+
if (row.status !== "pending" || row.revision !== selected.revision)
|
|
342
|
+
refuse("The connection changed; review it again before saving");
|
|
343
|
+
return "pending";
|
|
344
|
+
}
|
|
345
|
+
/** Used inside an authorized operation; verification reads stay inside its driver step. */
|
|
346
|
+
async activate(ref, revision, actor) {
|
|
347
|
+
const setup = await this.setup({ connection: ref }, actor, "activate");
|
|
348
|
+
const row = await this.row(setup.id);
|
|
349
|
+
if (!row ||
|
|
350
|
+
row.revision !== revision ||
|
|
351
|
+
row.definition !== setup.definition ||
|
|
352
|
+
!["pending", "uncertain"].includes(row.status))
|
|
353
|
+
refuse("The connection changed; review it again before activating");
|
|
354
|
+
const method = this.catalog.methods.get(row.vendor)[row.method];
|
|
355
|
+
const bundle = row.credential ? await this.secrets?.read(row.credential) : undefined;
|
|
356
|
+
if (row.credential && !bundle)
|
|
357
|
+
throw new SecretStoreError("unavailable");
|
|
358
|
+
const identity = await verifyAuthentication(method, bundle && decodeCredentials(method, bundle.value));
|
|
359
|
+
if (identity.account !== row.account)
|
|
360
|
+
throw new AuthenticationError("wrong_account");
|
|
361
|
+
if (JSON.stringify(identity.scopes) !== JSON.stringify([...new Set(row.scopes)].toSorted()))
|
|
362
|
+
throw new AuthenticationError("scope_changed");
|
|
363
|
+
const next = randomUUID();
|
|
364
|
+
const result = await (await this.db()).query("UPDATE sanoma_connections SET status='connected',revision=$3,updated_by=$4,updated_at=now() WHERE id=$1 AND revision=$2 RETURNING id", [row.id, revision, next, actor.id]);
|
|
365
|
+
if (!result.rowCount)
|
|
366
|
+
refuse("The connection changed; review it again before activating");
|
|
367
|
+
await this.health(row.id, next, "connected", bundle?.version);
|
|
368
|
+
return snapshot({ ...row, revision: next, status: "connected" });
|
|
369
|
+
}
|
|
370
|
+
bindingFingerprint(declaration, method, credential) {
|
|
371
|
+
const name = credential ?? ("credentials" in declaration ? declaration.credentials?.$secret : undefined);
|
|
372
|
+
const location = name ? this.secrets?.location(name) : undefined;
|
|
373
|
+
return createHash("sha256")
|
|
374
|
+
.update(JSON.stringify({ contract: fingerprint(declaration, method), location }))
|
|
375
|
+
.digest("hex");
|
|
376
|
+
}
|
|
377
|
+
checkStore(method, credential) {
|
|
378
|
+
if (!this.secrets || !credential)
|
|
379
|
+
return;
|
|
380
|
+
const location = this.secrets.location(credential);
|
|
381
|
+
const caps = location.namespace === ""
|
|
382
|
+
? {
|
|
383
|
+
writable: false,
|
|
384
|
+
maxBytes: Number.MAX_SAFE_INTEGER,
|
|
385
|
+
conditionalWrites: "none",
|
|
386
|
+
deletion: "unsupported",
|
|
387
|
+
changeDetection: "read",
|
|
388
|
+
}
|
|
389
|
+
: this.secrets.store.capabilities;
|
|
390
|
+
checkAuthenticationStore(method, caps, location.management);
|
|
391
|
+
}
|
|
392
|
+
async row(id) {
|
|
393
|
+
return (await (await this.db()).query("SELECT * FROM sanoma_connections WHERE id=$1", [id])).rows[0];
|
|
394
|
+
}
|
|
395
|
+
async select(vendor, ref, actor) {
|
|
396
|
+
if (!ref) {
|
|
397
|
+
const defaults = [...this.catalog.definitions.values()].filter((d) => d.vendor === vendor && d.default);
|
|
398
|
+
if (defaults[0])
|
|
399
|
+
ref = defaults[0];
|
|
400
|
+
else if (Object.keys(this.catalog.methods.get(vendor) ?? {}).length ||
|
|
401
|
+
[...this.catalog.definitions.values()].some((d) => d.vendor === vendor))
|
|
402
|
+
refuse("Select a named connection or declare an explicit default");
|
|
403
|
+
else
|
|
404
|
+
return undefined; // Operations without an authentication method need no connection.
|
|
405
|
+
}
|
|
406
|
+
if (!ref || ref.vendor !== vendor)
|
|
407
|
+
refuse("The selected connection belongs to another connector");
|
|
408
|
+
const row = await this.row(ref.$connection);
|
|
409
|
+
if (!row || row.vendor !== vendor || row.status !== "connected")
|
|
410
|
+
refuse("The connection is missing or disconnected");
|
|
411
|
+
if (ref.revision !== undefined && ref.revision !== row.revision)
|
|
412
|
+
refuse("The connection changed after authorization; start a new call");
|
|
413
|
+
const declaration = row.rule ? this.catalog.rules.get(row.rule) : this.catalog.definitions.get(row.id);
|
|
414
|
+
if (!declaration || declaration.vendor !== vendor || declaration.method !== row.method)
|
|
415
|
+
refuse("The connection is not declared");
|
|
416
|
+
if (row.rule && row.owner !== actor)
|
|
417
|
+
throw new SanomaError("policy_denied", "The connection belongs to another principal");
|
|
418
|
+
const method = this.catalog.methods.get(vendor)?.[row.method];
|
|
419
|
+
if (!method || row.definition !== this.bindingFingerprint(declaration, method, row.credential))
|
|
420
|
+
refuse("The connection declaration changed; reconnect before use");
|
|
421
|
+
this.checkStore(method, row.credential);
|
|
422
|
+
return snapshot(row);
|
|
423
|
+
}
|
|
424
|
+
async assertCurrent(selected, actor) {
|
|
425
|
+
if (!selected)
|
|
426
|
+
return;
|
|
427
|
+
const current = await this.select(selected.vendor, { vendor: selected.vendor, $connection: selected.id }, actor);
|
|
428
|
+
if (!current ||
|
|
429
|
+
current.revision !== selected.revision ||
|
|
430
|
+
current.definition !== selected.definition ||
|
|
431
|
+
current.method !== selected.method ||
|
|
432
|
+
current.account !== selected.account ||
|
|
433
|
+
current.credentials?.$secret !== selected.credentials?.$secret ||
|
|
434
|
+
JSON.stringify(current.scopes.toSorted()) !== JSON.stringify(selected.scopes.toSorted()))
|
|
435
|
+
refuse("The connection changed after authorization; start a new call");
|
|
436
|
+
}
|
|
437
|
+
/** Only called inside an authorized driver boundary. No value escapes into workflow history. */
|
|
438
|
+
async authenticate(selected, actor) {
|
|
439
|
+
await this.assertCurrent(selected, actor);
|
|
440
|
+
const method = this.catalog.methods.get(selected.vendor)?.[selected.method];
|
|
441
|
+
if (!method?.verify)
|
|
442
|
+
throw new AuthenticationError("unsupported");
|
|
443
|
+
const material = { method: selected.method, kind: method.kind };
|
|
444
|
+
if (method.kind !== "workload-identity") {
|
|
445
|
+
if (!this.secrets || !selected.credentials)
|
|
446
|
+
throw new SecretStoreError("unavailable");
|
|
447
|
+
const bundle = await this.secrets.read(selected.credentials.$secret);
|
|
448
|
+
if (!bundle)
|
|
449
|
+
throw new AuthenticationError("missing");
|
|
450
|
+
material.credentials = decodeCredentials(method, bundle.value);
|
|
451
|
+
material.credentialVersion = bundle.version;
|
|
452
|
+
}
|
|
453
|
+
const identity = await verifyAuthentication(method, material.credentials);
|
|
454
|
+
if (identity.account !== selected.account)
|
|
455
|
+
throw new AuthenticationError("wrong_account");
|
|
456
|
+
if (JSON.stringify(identity.scopes) !== JSON.stringify([...new Set(selected.scopes)].toSorted()))
|
|
457
|
+
throw new AuthenticationError("scope_changed");
|
|
458
|
+
// A disconnect or rebind while verification was in flight revokes this invocation too.
|
|
459
|
+
await this.assertCurrent(selected, actor);
|
|
460
|
+
return material;
|
|
461
|
+
}
|
|
462
|
+
async bind(ref, account, scopes, expectedRevision, actor) {
|
|
463
|
+
const declaration = this.catalog.definitions.get(ref.$connection);
|
|
464
|
+
if (!declaration || declaration.vendor !== ref.vendor)
|
|
465
|
+
refuse("The connection is not declared");
|
|
466
|
+
if (declaration.account && account !== declaration.account)
|
|
467
|
+
refuse("The account does not match the connection requirement");
|
|
468
|
+
if (declaration.scopes.some((s) => !scopes.includes(s)))
|
|
469
|
+
refuse("The account has not granted the required scopes");
|
|
470
|
+
const method = this.catalog.methods.get(ref.vendor)[declaration.method];
|
|
471
|
+
const row = {
|
|
472
|
+
id: ref.$connection,
|
|
473
|
+
vendor: ref.vendor,
|
|
474
|
+
method: declaration.method,
|
|
475
|
+
owner: null,
|
|
476
|
+
rule: null,
|
|
477
|
+
account,
|
|
478
|
+
scopes,
|
|
479
|
+
revision: randomUUID(),
|
|
480
|
+
definition: this.bindingFingerprint(declaration, method),
|
|
481
|
+
credential: declaration.credentials?.$secret ?? null,
|
|
482
|
+
status: "connected",
|
|
483
|
+
};
|
|
484
|
+
this.checkStore(method, row.credential);
|
|
485
|
+
await this.save(row, expectedRevision, actor);
|
|
486
|
+
return snapshot(row);
|
|
487
|
+
}
|
|
488
|
+
async create(ruleId, account, scopes, actor) {
|
|
489
|
+
const rule = this.catalog.rules.get(ruleId);
|
|
490
|
+
const person = typeof actor === "string" ? { id: actor } : actor;
|
|
491
|
+
if (!rule ||
|
|
492
|
+
!rule.createBy.some((by) => (typeof by === "string" ? by === person.id : person.groups?.includes(by.group))))
|
|
493
|
+
throw new SanomaError("policy_denied", "This principal cannot create connections under this rule");
|
|
494
|
+
if (rule.scopes.some((s) => !scopes.includes(s)))
|
|
495
|
+
refuse("The account has not granted the required scopes");
|
|
496
|
+
const method = this.catalog.methods.get(rule.vendor)[rule.method];
|
|
497
|
+
const id = `${ruleId}/${randomUUID()}`;
|
|
498
|
+
const credential = method.kind === "workload-identity" ? null : `CONNECTION_${randomUUID().replaceAll("-", "").toUpperCase()}`;
|
|
499
|
+
const row = {
|
|
500
|
+
id,
|
|
501
|
+
vendor: rule.vendor,
|
|
502
|
+
method: rule.method,
|
|
503
|
+
owner: person.id,
|
|
504
|
+
rule: ruleId,
|
|
505
|
+
account,
|
|
506
|
+
scopes,
|
|
507
|
+
revision: randomUUID(),
|
|
508
|
+
definition: this.bindingFingerprint(rule, method, credential),
|
|
509
|
+
credential,
|
|
510
|
+
status: "connected",
|
|
511
|
+
};
|
|
512
|
+
this.checkStore(method, row.credential);
|
|
513
|
+
await this.save(row, null, person.id);
|
|
514
|
+
return snapshot(row);
|
|
515
|
+
}
|
|
516
|
+
async disconnect(ref, expectedRevision, actor) {
|
|
517
|
+
const row = await this.row(ref.$connection);
|
|
518
|
+
if (!row ||
|
|
519
|
+
row.vendor !== ref.vendor ||
|
|
520
|
+
(row.rule ? !this.catalog.rules.has(row.rule) : !this.catalog.definitions.has(row.id)))
|
|
521
|
+
refuse("The connection is not declared");
|
|
522
|
+
if (row.rule && row.owner !== actor)
|
|
523
|
+
throw new SanomaError("policy_denied", "The connection belongs to another principal");
|
|
524
|
+
const revision = randomUUID();
|
|
525
|
+
const changed = await (await this.db()).query("UPDATE sanoma_connections SET status='disconnected',revision=$2,updated_by=$3,updated_at=now() WHERE id=$1 AND revision=$4 RETURNING id", [row.id, revision, actor, expectedRevision]);
|
|
526
|
+
if (!changed.rowCount)
|
|
527
|
+
refuse("The connection changed; reload before disconnecting it");
|
|
528
|
+
return { id: row.id, revision, status: "disconnected" };
|
|
529
|
+
}
|
|
530
|
+
async save(row, expected, actor) {
|
|
531
|
+
const values = [
|
|
532
|
+
row.id,
|
|
533
|
+
row.vendor,
|
|
534
|
+
row.method,
|
|
535
|
+
row.owner,
|
|
536
|
+
row.rule,
|
|
537
|
+
row.account,
|
|
538
|
+
JSON.stringify(row.scopes),
|
|
539
|
+
row.revision,
|
|
540
|
+
row.definition,
|
|
541
|
+
row.credential,
|
|
542
|
+
actor,
|
|
543
|
+
row.status,
|
|
544
|
+
row.credential && this.secrets ? JSON.stringify(this.secrets.location(row.credential)) : null,
|
|
545
|
+
];
|
|
546
|
+
const query = expected === null
|
|
547
|
+
? "INSERT INTO sanoma_connections (id,vendor,method,owner,rule,account,scopes,revision,definition,credential,updated_by,status,location) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT(id) DO NOTHING RETURNING id"
|
|
548
|
+
: "UPDATE sanoma_connections SET vendor=$2,method=$3,owner=$4,rule=$5,account=$6,scopes=$7,revision=$8,definition=$9,credential=$10,updated_by=$11,updated_at=now(),status=$12,location=$13 WHERE id=$1 AND revision=$14 RETURNING id";
|
|
549
|
+
const result = await (await this.db()).query(query, [...values, ...(expected === null ? [] : [expected])]);
|
|
550
|
+
if (!result.rowCount)
|
|
551
|
+
refuse("The connection changed; reload before binding it");
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/** Metadata mutations are access operations, so they are policy-reviewed and audited like other effects. */
|
|
555
|
+
export const connectionOps = defineConnector("sanoma", {
|
|
556
|
+
connection: {
|
|
557
|
+
test: {
|
|
558
|
+
effect: "read",
|
|
559
|
+
input: z.object({ connection: connectionRefSchema }),
|
|
560
|
+
output: z.object({
|
|
561
|
+
state: z.enum([
|
|
562
|
+
"missing",
|
|
563
|
+
"expired",
|
|
564
|
+
"revoked",
|
|
565
|
+
"rejected",
|
|
566
|
+
"wrong_account",
|
|
567
|
+
"scope_changed",
|
|
568
|
+
"uncertain",
|
|
569
|
+
"unavailable",
|
|
570
|
+
"unsupported",
|
|
571
|
+
"connected",
|
|
572
|
+
"disconnected",
|
|
573
|
+
"pending",
|
|
574
|
+
"configuration_changed",
|
|
575
|
+
"store_denied",
|
|
576
|
+
"invalid",
|
|
577
|
+
]),
|
|
578
|
+
checkedAt: z.string(),
|
|
579
|
+
externallyRenewed: z.boolean(),
|
|
580
|
+
}),
|
|
581
|
+
target: (input) => input.connection.$connection,
|
|
582
|
+
},
|
|
583
|
+
prepare: {
|
|
584
|
+
effect: "access",
|
|
585
|
+
input: z.object({
|
|
586
|
+
target: connectionTargetSchema,
|
|
587
|
+
requestId: z.string().min(1),
|
|
588
|
+
definition: z.string(),
|
|
589
|
+
authorization: z.string(),
|
|
590
|
+
}),
|
|
591
|
+
output: setupSchema,
|
|
592
|
+
target: (input) => "connection" in input.target ? input.target.connection.$connection : input.target.rule,
|
|
593
|
+
},
|
|
594
|
+
configure: {
|
|
595
|
+
effect: "access",
|
|
596
|
+
input: z.object({
|
|
597
|
+
target: connectionTargetSchema,
|
|
598
|
+
requestId: z.string().min(1),
|
|
599
|
+
expectedRevision: z.string().nullable(),
|
|
600
|
+
definition: z.string(),
|
|
601
|
+
account: z.string().min(1),
|
|
602
|
+
scopes: z.array(z.string()),
|
|
603
|
+
}),
|
|
604
|
+
output: snapshotSchema,
|
|
605
|
+
target: (input) => "connection" in input.target ? input.target.connection.$connection : input.target.rule,
|
|
606
|
+
},
|
|
607
|
+
activate: {
|
|
608
|
+
effect: "access",
|
|
609
|
+
input: z.object({ connection: connectionRefSchema, expectedRevision: z.string() }),
|
|
610
|
+
output: snapshotSchema,
|
|
611
|
+
target: (input) => input.connection.$connection,
|
|
612
|
+
},
|
|
613
|
+
disconnect: {
|
|
614
|
+
effect: "access",
|
|
615
|
+
input: z.object({ connection: connectionRefSchema, expectedRevision: z.string() }),
|
|
616
|
+
output: z.object({ id: z.string(), revision: z.string(), status: z.literal("disconnected") }),
|
|
617
|
+
target: (input) => input.connection.$connection,
|
|
618
|
+
},
|
|
619
|
+
bind: {
|
|
620
|
+
effect: "access",
|
|
621
|
+
input: z.object({
|
|
622
|
+
connection: connectionRefSchema,
|
|
623
|
+
account: z.string().min(1),
|
|
624
|
+
scopes: z.array(z.string()),
|
|
625
|
+
expectedRevision: z.string().nullable(),
|
|
626
|
+
}),
|
|
627
|
+
output: snapshotSchema,
|
|
628
|
+
target: (input) => input.connection.$connection,
|
|
629
|
+
},
|
|
630
|
+
create: {
|
|
631
|
+
effect: "access",
|
|
632
|
+
input: z.object({ rule: z.string(), account: z.string().min(1), scopes: z.array(z.string()) }),
|
|
633
|
+
output: snapshotSchema,
|
|
634
|
+
target: (input) => input.rule,
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
});
|
|
638
|
+
const actor = (call) => call.principal ?? refuse("Connection mutation needs an authenticated principal");
|
|
639
|
+
export function connectionDriver(registry) {
|
|
640
|
+
return defineDriver(connectionOps, {
|
|
641
|
+
connection: {
|
|
642
|
+
test: (input, call) => registry.test(input.connection, actor(call)),
|
|
643
|
+
prepare: async (input, call) => {
|
|
644
|
+
const setup = await registry.setup(input.target, actor(call), input.requestId);
|
|
645
|
+
if (setup.definition !== input.definition)
|
|
646
|
+
refuse("The connection changed; review its requirements again");
|
|
647
|
+
return { ...setup, requiredScopes: [...setup.requiredScopes] };
|
|
648
|
+
},
|
|
649
|
+
configure: (input, call) => registry.configure(input.target, input.requestId, input.expectedRevision, input.definition, input.account, input.scopes, actor(call)),
|
|
650
|
+
activate: (input, call) => registry.activate(input.connection, input.expectedRevision, actor(call)),
|
|
651
|
+
disconnect: (input, call) => registry.disconnect(input.connection, input.expectedRevision, actor(call).id),
|
|
652
|
+
bind: (input, call) => registry.bind(input.connection, input.account, input.scopes, input.expectedRevision, actor(call).id),
|
|
653
|
+
create: (input, call) => registry.create(input.rule, input.account, input.scopes, actor(call)),
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
//# sourceMappingURL=connection.js.map
|