@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/api/integrations.d.ts +7 -1
- package/dist/api/integrations.js +9 -0
- package/dist/docs/API_SUMMARY.md +87 -300
- package/dist/docs/deploying-apps.md +193 -0
- package/dist/docs/server-functions.md +306 -0
- package/dist/openapi.yaml +171 -0
- package/dist/testing/index.d.ts +64 -0
- package/dist/testing/index.js +145 -0
- package/dist/types/appManifest.d.ts +114 -0
- package/dist/types/integrations.d.ts +8 -0
- package/docs/API_SUMMARY.md +87 -300
- package/docs/deploying-apps.md +193 -0
- package/docs/server-functions.md +306 -0
- package/openapi.yaml +171 -0
- package/package.json +7 -2
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
|
+
}
|