@proveanything/smartlinks 1.17.5 → 2.0.0-alpha.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/dist/openapi.yaml CHANGED
@@ -38,6 +38,7 @@ tags:
38
38
  - name: crate
39
39
  - name: facets
40
40
  - name: form
41
+ - name: integrations
41
42
  - name: interactions
42
43
  - name: jobs
43
44
  - name: journeys
@@ -4164,6 +4165,33 @@ paths:
4164
4165
  description: Unauthorized
4165
4166
  404:
4166
4167
  description: Not found
4168
+ /admin/collection/{collectionId}/integrations/record-types:
4169
+ get:
4170
+ tags:
4171
+ - integrations
4172
+ summary: Discover the app-record types present in a collection + which app owns each (introspected), for picking a sub-record source/trigger.
4173
+ operationId: integrations_listRecordTypes
4174
+ security:
4175
+ - bearerAuth: []
4176
+ parameters:
4177
+ - name: collectionId
4178
+ in: path
4179
+ required: true
4180
+ schema:
4181
+ type: string
4182
+ responses:
4183
+ 200:
4184
+ description: Success
4185
+ content:
4186
+ application/json:
4187
+ schema:
4188
+ $ref: "#/components/schemas/RecordTypesResponse"
4189
+ 400:
4190
+ description: Bad request
4191
+ 401:
4192
+ description: Unauthorized
4193
+ 404:
4194
+ description: Not found
4167
4195
  /admin/collection/{collectionId}/interactions:
4168
4196
  get:
4169
4197
  tags:
@@ -17215,6 +17243,109 @@ components:
17215
17243
  required:
17216
17244
  - files
17217
17245
  - function
17246
+ AppFunctionTrigger:
17247
+ type: object
17248
+ properties:
17249
+ type:
17250
+ $ref: "#/components/schemas/AppFunctionTriggerType"
17251
+ eventTypes:
17252
+ type: array
17253
+ items:
17254
+ type: string
17255
+ schedule:
17256
+ type: string
17257
+ route:
17258
+ type: string
17259
+ methods:
17260
+ type: array
17261
+ items:
17262
+ type: string
17263
+ enum:
17264
+ - GET
17265
+ - POST
17266
+ - PUT
17267
+ - PATCH
17268
+ - DELETE
17269
+ required:
17270
+ - type
17271
+ AppFunctionDef:
17272
+ type: object
17273
+ properties:
17274
+ name:
17275
+ type: string
17276
+ description:
17277
+ type: string
17278
+ trigger:
17279
+ $ref: "#/components/schemas/AppFunctionTrigger"
17280
+ visibility:
17281
+ $ref: "#/components/schemas/AppFunctionVisibility"
17282
+ authority:
17283
+ $ref: "#/components/schemas/AppFunctionAuthority"
17284
+ elevated:
17285
+ type: boolean
17286
+ capabilities:
17287
+ type: array
17288
+ items:
17289
+ type: string
17290
+ apiVersion:
17291
+ type: string
17292
+ handler:
17293
+ type: string
17294
+ required:
17295
+ - name
17296
+ - trigger
17297
+ AppManifestFunctions:
17298
+ type: object
17299
+ properties:
17300
+ files:
17301
+ $ref: "#/components/schemas/AppManifestFiles"
17302
+ definitions:
17303
+ type: array
17304
+ items:
17305
+ $ref: "#/components/schemas/AppFunctionDef"
17306
+ required:
17307
+ - files
17308
+ - definitions
17309
+ ServerFunctionCaller:
17310
+ type: object
17311
+ properties:
17312
+ userId:
17313
+ type: string
17314
+ anonymous:
17315
+ type: boolean
17316
+ origin:
17317
+ type: string
17318
+ ip:
17319
+ type: string
17320
+ via:
17321
+ $ref: "#/components/schemas/AppFunctionTriggerType"
17322
+ required:
17323
+ - userId
17324
+ - anonymous
17325
+ - via
17326
+ ServerFunctionContext:
17327
+ type: object
17328
+ properties:
17329
+ collectionId:
17330
+ type: string
17331
+ appId:
17332
+ type: string
17333
+ sl: {}
17334
+ secrets:
17335
+ type: object
17336
+ additionalProperties: true
17337
+ caller:
17338
+ $ref: "#/components/schemas/ServerFunctionCaller"
17339
+ fetch:
17340
+ type: object
17341
+ additionalProperties: true
17342
+ required:
17343
+ - collectionId
17344
+ - appId
17345
+ - sl
17346
+ - secrets
17347
+ - caller
17348
+ - fetch
17218
17349
  AppAdminConfig:
17219
17350
  type: object
17220
17351
  properties:
@@ -17357,6 +17488,8 @@ components:
17357
17488
  $ref: "#/components/schemas/DeepLinkEntry"
17358
17489
  executor:
17359
17490
  $ref: "#/components/schemas/AppManifestExecutor"
17491
+ functions:
17492
+ $ref: "#/components/schemas/AppManifestFunctions"
17360
17493
  required:
17361
17494
  - name
17362
17495
  - version
@@ -17397,6 +17530,22 @@ components:
17397
17530
  properties:
17398
17531
  force:
17399
17532
  type: boolean
17533
+ AppFunctionTriggerType:
17534
+ type: string
17535
+ enum:
17536
+ - http
17537
+ - event
17538
+ - cron
17539
+ AppFunctionVisibility:
17540
+ type: string
17541
+ enum:
17542
+ - admin
17543
+ - public
17544
+ AppFunctionAuthority:
17545
+ type: string
17546
+ enum:
17547
+ - caller
17548
+ - collection
17400
17549
  PaginatedResponse:
17401
17550
  type: object
17402
17551
  properties:
@@ -23429,6 +23578,28 @@ components:
23429
23578
  properties:
23430
23579
  purpose:
23431
23580
  type: string
23581
+ RecordTypeInfo:
23582
+ type: object
23583
+ properties:
23584
+ appId:
23585
+ type: string
23586
+ recordType:
23587
+ type: string
23588
+ count:
23589
+ type: number
23590
+ required:
23591
+ - appId
23592
+ - recordType
23593
+ - count
23594
+ RecordTypesResponse:
23595
+ type: object
23596
+ properties:
23597
+ recordTypes:
23598
+ type: array
23599
+ items:
23600
+ $ref: "#/components/schemas/RecordTypeInfo"
23601
+ required:
23602
+ - recordTypes
23432
23603
  RunFlowResult:
23433
23604
  type: object
23434
23605
  additionalProperties: true
@@ -0,0 +1,64 @@
1
+ import type { AppFunctionDef, ServerFunctionContext } from '../types/appManifest';
2
+ export declare class CapabilityError extends Error {
3
+ capability: string;
4
+ code: string;
5
+ constructor(capability: string, message?: string);
6
+ }
7
+ /** The surface `ctx.sl` exposes — mirrors the server facade. Provide the methods your test needs. */
8
+ export interface TestSlImpl {
9
+ appRecords?: {
10
+ create?(fields: any): any;
11
+ update?(id: string, fields: any): any;
12
+ upsert?(fields: any): any;
13
+ delete?(id: string): any;
14
+ get?(id: string): any;
15
+ query?(params: any): any;
16
+ listTypes?(): any;
17
+ };
18
+ products?: {
19
+ get?(id: string, opts?: any): any;
20
+ query?(body?: any, opts?: any): any;
21
+ create?(data: any, opts?: any): any;
22
+ update?(id: string, data: any, opts?: any): any;
23
+ };
24
+ attestations?: {
25
+ create?(fields: any): any;
26
+ };
27
+ }
28
+ export interface TestCaller {
29
+ userId?: string | null;
30
+ anonymous?: boolean;
31
+ origin?: string | null;
32
+ ip?: string | null;
33
+ }
34
+ export interface CreateFunctionTestContextOptions {
35
+ /** The manifest declaration under test — its `capabilities` are the enforced envelope. */
36
+ def: Pick<AppFunctionDef, 'capabilities' | 'trigger' | 'visibility' | 'authority'>;
37
+ collectionId?: string;
38
+ appId?: string;
39
+ caller?: TestCaller;
40
+ /** Fixture secrets, keyed by ref. Real sealed secrets are server-only and never available locally. */
41
+ secrets?: Record<string, string>;
42
+ /** Backing impl for ctx.sl. Omit for pure unit tests (methods return a stub echo). */
43
+ sl?: TestSlImpl;
44
+ /** Backing fetch (defaults to global fetch). Still gated by the `network` capability. */
45
+ fetch?: typeof fetch;
46
+ }
47
+ export interface FunctionTestContext extends ServerFunctionContext {
48
+ /** Captured log lines (also written via ctx.log). */
49
+ logs: Array<{
50
+ at: string;
51
+ message: string;
52
+ data?: Record<string, any>;
53
+ }>;
54
+ }
55
+ /**
56
+ * Build a capability-enforcing test ctx for a server function. Run your handler with it:
57
+ *
58
+ * const ctx = createFunctionTestContext({ def, caller: { userId: 'me' }, secrets: { k: 'v' } })
59
+ * const result = await myHandler(ctx, { method: 'POST', body: { … } })
60
+ *
61
+ * A ctx.sl / ctx.secrets / ctx.fetch call not covered by `def.capabilities` throws
62
+ * CapabilityError — exactly as it would in production.
63
+ */
64
+ export declare function createFunctionTestContext(opts: CreateFunctionTestContextOptions): FunctionTestContext;
@@ -0,0 +1,145 @@
1
+ // src/testing/index.ts
2
+ //
3
+ // Local test harness for SmartLinks server functions — importable as
4
+ // `@proveanything/smartlinks/testing`. It builds a `ctx` that matches the runtime
5
+ // contract AND enforces the declared capability envelope, so a function fails locally
6
+ // the same way it would after deploy — no "deploy and pray".
7
+ //
8
+ // Fidelity (documented in docs/server-functions.md "Testing & preview"):
9
+ // - Capabilities are enforced EXACTLY as declared in the manifest `def` — pass the def
10
+ // itself so "tested" can't drift from "declared".
11
+ // - `ctx.sl` delegates to an impl you inject: your live SDK for real reads/writes, or
12
+ // omit it for pure unit tests (methods return a stub result instead of hitting the
13
+ // network — control flow + capability enforcement still run).
14
+ // - `ctx.secrets` are FIXTURES you provide; real sealed secrets are server-only and
15
+ // never resolvable locally.
16
+ // - `ctx.fetch` is gated by the `network` capability just like production.
17
+ export class CapabilityError extends Error {
18
+ constructor(capability, message) {
19
+ super(message || `capability not granted: ${capability}`);
20
+ this.code = 'CAPABILITY_DENIED';
21
+ this.name = 'CapabilityError';
22
+ this.capability = capability;
23
+ }
24
+ }
25
+ // The SAME grammar the server enforces (prove server/services/functions/validate.js).
26
+ function parseCapabilities(list = []) {
27
+ const sl = new Set();
28
+ const secrets = new Set();
29
+ const networkHosts = new Set();
30
+ let networkAll = false;
31
+ for (const raw of list || []) {
32
+ const cap = String(raw || '').trim();
33
+ if (!cap)
34
+ continue;
35
+ if (cap === 'network') {
36
+ networkAll = true;
37
+ continue;
38
+ }
39
+ if (cap.startsWith('network:')) {
40
+ networkHosts.add(cap.slice(8).toLowerCase());
41
+ continue;
42
+ }
43
+ if (cap.startsWith('secrets:')) {
44
+ secrets.add(cap.slice(8));
45
+ continue;
46
+ }
47
+ if (cap.startsWith('sl:')) {
48
+ sl.add(cap.slice(3));
49
+ continue;
50
+ }
51
+ }
52
+ return { sl, secrets, networkAll, networkHosts };
53
+ }
54
+ function allowsSl(p, resource, op) {
55
+ if (p.sl.has(`${resource}:${op}`))
56
+ return true;
57
+ if (op === 'read' && p.sl.has(`${resource}:write`))
58
+ return true; // write implies read
59
+ return false;
60
+ }
61
+ const stub = (method, args) => ({ __stub: true, method, args });
62
+ /**
63
+ * Build a capability-enforcing test ctx for a server function. Run your handler with it:
64
+ *
65
+ * const ctx = createFunctionTestContext({ def, caller: { userId: 'me' }, secrets: { k: 'v' } })
66
+ * const result = await myHandler(ctx, { method: 'POST', body: { … } })
67
+ *
68
+ * A ctx.sl / ctx.secrets / ctx.fetch call not covered by `def.capabilities` throws
69
+ * CapabilityError — exactly as it would in production.
70
+ */
71
+ export function createFunctionTestContext(opts) {
72
+ var _a, _b, _c, _d;
73
+ const def = opts.def || {};
74
+ const parsed = parseCapabilities(def.capabilities || []);
75
+ const impl = opts.sl || {};
76
+ const logs = [];
77
+ const gated = (resource, op, fn, name) => async (...args) => {
78
+ if (!allowsSl(parsed, resource, op))
79
+ throw new CapabilityError(`sl:${resource}:${op}`);
80
+ return fn ? fn(...args) : stub(`${resource}.${name}`, args);
81
+ };
82
+ const ar = impl.appRecords || {};
83
+ const pr = impl.products || {};
84
+ const at = impl.attestations || {};
85
+ const sl = {
86
+ appRecords: {
87
+ create: gated('records', 'write', ar.create && ar.create.bind(ar), 'create'),
88
+ update: gated('records', 'write', ar.update && ar.update.bind(ar), 'update'),
89
+ upsert: gated('records', 'write', ar.upsert && ar.upsert.bind(ar), 'upsert'),
90
+ delete: gated('records', 'write', ar.delete && ar.delete.bind(ar), 'delete'),
91
+ get: gated('records', 'read', ar.get && ar.get.bind(ar), 'get'),
92
+ query: gated('records', 'read', ar.query && ar.query.bind(ar), 'query'),
93
+ listTypes: gated('records', 'read', ar.listTypes && ar.listTypes.bind(ar), 'listTypes'),
94
+ },
95
+ products: {
96
+ get: gated('products', 'read', pr.get && pr.get.bind(pr), 'get'),
97
+ query: gated('products', 'read', pr.query && pr.query.bind(pr), 'query'),
98
+ create: gated('products', 'write', pr.create && pr.create.bind(pr), 'create'),
99
+ update: gated('products', 'write', pr.update && pr.update.bind(pr), 'update'),
100
+ },
101
+ attestations: {
102
+ create: gated('attestations', 'write', at.create && at.create.bind(at), 'create'),
103
+ },
104
+ };
105
+ const secretsMap = opts.secrets || {};
106
+ const baseFetch = opts.fetch || (typeof fetch !== 'undefined' ? fetch : undefined);
107
+ const via = (def.trigger && def.trigger.type) || 'http';
108
+ const caller = opts.caller || {};
109
+ return {
110
+ collectionId: opts.collectionId || 'test-collection',
111
+ appId: opts.appId || 'test-app',
112
+ sl,
113
+ secrets: {
114
+ async get(ref) {
115
+ if (!parsed.secrets.has(ref))
116
+ throw new CapabilityError(`secrets:${ref}`);
117
+ return Object.prototype.hasOwnProperty.call(secretsMap, ref) ? secretsMap[ref] : null;
118
+ },
119
+ },
120
+ caller: {
121
+ userId: (_a = caller.userId) !== null && _a !== void 0 ? _a : null,
122
+ anonymous: (_b = caller.anonymous) !== null && _b !== void 0 ? _b : !caller.userId,
123
+ origin: (_c = caller.origin) !== null && _c !== void 0 ? _c : null,
124
+ ip: (_d = caller.ip) !== null && _d !== void 0 ? _d : null,
125
+ via,
126
+ },
127
+ fetch: (async (input, init) => {
128
+ let host = '';
129
+ try {
130
+ host = new URL(typeof input === 'string' ? input : input.url).host;
131
+ }
132
+ catch ( /* bad URL → denied */_a) { /* bad URL → denied */ }
133
+ const allowed = parsed.networkAll || (!!host && parsed.networkHosts.has(host.toLowerCase()));
134
+ if (!allowed)
135
+ throw new CapabilityError(host ? `network:${host}` : 'network');
136
+ if (!baseFetch)
137
+ throw new Error('fetch is not available in this environment; pass opts.fetch');
138
+ return baseFetch(input, init);
139
+ }),
140
+ log: (message, data) => {
141
+ logs.push(Object.assign({ at: new Date().toISOString(), message: String(message) }, (data ? { data } : {})));
142
+ },
143
+ logs,
144
+ };
145
+ }
@@ -204,6 +204,113 @@ export interface AppManifestExecutor {
204
204
  responseShape?: Record<string, any>;
205
205
  };
206
206
  }
207
+ /** What causes a server function to run. */
208
+ export type AppFunctionTriggerType = 'http' | 'event' | 'cron';
209
+ export interface AppFunctionTrigger {
210
+ type: AppFunctionTriggerType;
211
+ /** `event`: event types this function subscribes to, e.g. `['interaction.submitted:comp-entry']`. */
212
+ eventTypes?: string[];
213
+ /** `cron`: standard 5-field crontab expression, evaluated in UTC. */
214
+ schedule?: string;
215
+ /** `http`: URL path segment the function is exposed at (defaults to the function `name`). */
216
+ route?: string;
217
+ /** `http`: accepted HTTP methods (defaults to `['POST']`). */
218
+ methods?: Array<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'>;
219
+ }
220
+ /** Who is allowed to invoke an `http`-triggered function. */
221
+ export type AppFunctionVisibility = 'admin' | 'public';
222
+ /**
223
+ * Whose authority the function runs with — i.e. what `ctx.sl` can do.
224
+ * - `caller` — runs as the invoking user (their session/JWT). The secure default.
225
+ * - `collection` — runs with collection-admin authority over THIS collection only
226
+ * (never a global superuser). Required for `public` functions that
227
+ * must perform a privileged server-side action; the author is then
228
+ * responsible for validating input and preventing abuse.
229
+ */
230
+ export type AppFunctionAuthority = 'caller' | 'collection';
231
+ /** One server function declared in the manifest. */
232
+ export interface AppFunctionDef {
233
+ /** Stable identifier, unique within the app, e.g. `'submitCompetitionEntry'`. */
234
+ name: string;
235
+ description?: string;
236
+ trigger: AppFunctionTrigger;
237
+ /**
238
+ * `http` only. `admin` = authenticated admin surface; `public` = publicly callable.
239
+ * Ignored for `event`/`cron` (no external caller). Defaults to `admin`.
240
+ */
241
+ visibility?: AppFunctionVisibility;
242
+ /**
243
+ * Whose authority `ctx.sl` carries. Defaults to `caller` (secure by default).
244
+ * `event`/`cron` functions have no caller and always run as `collection`.
245
+ */
246
+ authority?: AppFunctionAuthority;
247
+ /**
248
+ * Required acknowledgment for the sharp edge: a `public` + `collection` function is
249
+ * publicly callable AND runs with elevated collection authority. Set `elevated: true`
250
+ * to confirm you intend that and accept responsibility for validating requests —
251
+ * without it, install/validation fails. Ignored for any other visibility/authority combo.
252
+ */
253
+ elevated?: boolean;
254
+ /**
255
+ * Least-privilege capabilities this function needs, surfaced at install for
256
+ * consent and capped at runtime — even for `collection`-authority functions.
257
+ * e.g. `['sl:records:write', 'network:api.example.com', 'secrets:syndigo-key']`.
258
+ * Grammar: `sl:<resource>:<read|write>`, `network` (or `network:<host>`), `secrets:<ref>`.
259
+ */
260
+ capabilities?: string[];
261
+ /** SDK/manifest API version this function targets (pinned for runtime compatibility). */
262
+ apiVersion?: string;
263
+ /** Exported handler name in the functions bundle. Defaults to `name`. */
264
+ handler?: string;
265
+ }
266
+ /** The `functions` block in `app.manifest.json`. Presence means the app ships server functions. */
267
+ export interface AppManifestFunctions {
268
+ files: AppManifestFiles;
269
+ definitions: AppFunctionDef[];
270
+ }
271
+ /** Identity of whoever invoked a server function. */
272
+ export interface ServerFunctionCaller {
273
+ /** Authenticated user id, or `null` for anonymous/public/system invocations. */
274
+ userId: string | null;
275
+ /** True when no authenticated user is present (public/anonymous call, or event/cron). */
276
+ anonymous: boolean;
277
+ /** Request origin/referer, when available (`http` trigger). */
278
+ origin?: string | null;
279
+ /** Client IP, when available (`http` trigger). */
280
+ ip?: string | null;
281
+ /** How the function was triggered. */
282
+ via: AppFunctionTriggerType;
283
+ }
284
+ /**
285
+ * The context handed to every SmartLinks server function. The platform builds a
286
+ * fresh one per invocation and pre-scopes each handle to the function's declared
287
+ * authority + capabilities. The function receives only handles already narrowed
288
+ * to what it declared — no raw keys, no ambient superuser client.
289
+ */
290
+ export interface ServerFunctionContext {
291
+ collectionId: string;
292
+ appId: string;
293
+ /**
294
+ * SmartLinks SDK, pre-scoped to the function's declared `authority`:
295
+ * - `caller` → scoped to the invoking user (their JWT).
296
+ * - `collection` → scoped to a collection-admin principal for THIS collection
297
+ * only — never a global superuser.
298
+ * Declared `capabilities` cap what these calls may do.
299
+ */
300
+ sl: any;
301
+ /** Capability-gated secret access. `get(ref)` resolves only refs granted via `secrets:<ref>`. */
302
+ secrets: {
303
+ get(ref: string): Promise<string | null>;
304
+ };
305
+ /** Who invoked this function. */
306
+ caller: ServerFunctionCaller;
307
+ /** Outbound HTTP — present only when the `network` capability is declared (host-scoped if `network:<host>`). */
308
+ fetch: typeof fetch;
309
+ /** Structured logging captured into run telemetry. */
310
+ log: (message: string, data?: Record<string, any>) => void;
311
+ }
312
+ /** The signature every SmartLinks server function implements. */
313
+ export type ServerFunctionHandler<TEvent = any, TResult = any> = (ctx: ServerFunctionContext, event: TEvent) => Promise<TResult> | TResult;
207
314
  /**
208
315
  * Shape of `app.admin.json` -- the separate admin configuration file pointed to
209
316
  * by `AppManifest.admin`. Fetch this file yourself when you need setup / import /
@@ -342,6 +449,13 @@ export interface AppManifest {
342
449
  * @see AppManifestExecutor
343
450
  */
344
451
  executor?: AppManifestExecutor;
452
+ /**
453
+ * Server functions ("edge functions") this app deploys into SmartLinks —
454
+ * arbitrary server-side code triggered by http, events, or cron, each running
455
+ * with a declared authority + least-privilege capabilities.
456
+ * @see AppManifestFunctions
457
+ */
458
+ functions?: AppManifestFunctions;
345
459
  [key: string]: any;
346
460
  }
347
461
  /**
@@ -134,3 +134,11 @@ export interface SetSecretResult {
134
134
  export interface ListSecretsQuery {
135
135
  purpose?: string;
136
136
  }
137
+ export interface RecordTypeInfo {
138
+ appId: string;
139
+ recordType: string;
140
+ count: number;
141
+ }
142
+ export interface RecordTypesResponse {
143
+ recordTypes: RecordTypeInfo[];
144
+ }