@substrat-run/contracts 0.153.0 → 0.154.0

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.
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Keys no person may hold (#2425, #2430): the one reading of `holder: 'system'` on a manifest's
3
+ * permission declaration, applied wherever a permission surface is judged. `lint:permissions`
4
+ * (`tools/reserved-keys.mts`), `buildPermissionRegistry` and so `substrat push`, the push boundary
5
+ * (`pushedPermissionRegistry`), and a scope host's role doors (the kernel's `reservedRoleRefusal`)
6
+ * all call these, so none of them can read the mark differently.
7
+ *
8
+ * A vertical's person-facing surface is its `ROLES` and its `ENTITY_GRANTS` shapes (what a
9
+ * principal is handed per entity). A module's SYSTEM principal is a different declaration:
10
+ * `manifest.schedules[].permissions` (`system:<module>`) and `manifest.peers[].permissions`
11
+ * (`vertical:<slug>`). So "a system-only key" needs no new concept in the surface: it may appear
12
+ * in the second place and never in the first. The one role that may carry one is a role declared
13
+ * `holder: 'system'` (K-55), and it may carry nothing else.
14
+ *
15
+ * This is NOT the purge-only notion of K-45 (`entity-trash.ts`): that scopes a key a system
16
+ * principal already holds to one schedule's call. It says where a system principal may USE a key,
17
+ * not that a person may not be given it.
18
+ *
19
+ * Pure and dependency-free, so `tools/` can load it from source.
20
+ */
21
+ /** What the manifests say about a key reserved to a system principal. */
22
+ export interface ReservedKey {
23
+ description: string;
24
+ /** Every module that declares the key `holder: 'system'`, sorted. */
25
+ declaredBy: string[];
26
+ }
27
+ /** The part of a module a reservation is read from: its manifest's id and permission declarations. */
28
+ export interface ReservedKeySource {
29
+ manifest: {
30
+ id: string;
31
+ permissions: readonly {
32
+ key: string;
33
+ description: string;
34
+ holder?: 'system';
35
+ }[];
36
+ /** What a fresh install's owner is granted on day one: a person's grant (#2430). */
37
+ ownerGrants?: readonly string[];
38
+ };
39
+ }
40
+ /**
41
+ * Every key a manifest declares `holder: 'system'`.
42
+ *
43
+ * A union, not a vote: one module declaring the key reserved reserves it, whatever another module
44
+ * declaring the same key says. Otherwise a vertical could lift an engine's reservation by
45
+ * re-declaring the key, unmarked, in a module of its own.
46
+ */
47
+ export declare function reservedKeysOf(modules: readonly ReservedKeySource[]): Map<string, ReservedKey>;
48
+ export interface ReservedKeyOffence {
49
+ /**
50
+ * Where it was granted: `role <key>`, `system-held role <key>`, `grant shape on <entityType>`, or
51
+ * a person grant's own name (`owner grants of <module>`, `owner grants (substrat.ownerGrants)`).
52
+ */
53
+ where: string;
54
+ key: string;
55
+ /** Set for a system-held role holding a key that is not reserved to a system principal. */
56
+ notReserved?: true;
57
+ /** Set for a system-held role that holds no reserved key at all. */
58
+ empty?: true;
59
+ }
60
+ /** The surface a reservation is judged against: the roles, the entity grant shapes, and person grants. */
61
+ export interface ReservedKeySurface {
62
+ roles: readonly {
63
+ key: string;
64
+ permissions: readonly string[];
65
+ holder?: 'person' | 'system';
66
+ }[];
67
+ grants: readonly {
68
+ entityType: string;
69
+ permissions: readonly string[];
70
+ }[];
71
+ /** #2430: keys a person is granted directly, each set named as the refusal names it (`ownerGrantSets`). */
72
+ personGrants?: readonly {
73
+ where: string;
74
+ permissions: readonly string[];
75
+ }[];
76
+ }
77
+ /**
78
+ * #2430: the owner grants a vertical declares, as named person grants — each module manifest's
79
+ * `ownerGrants` and, when given, the deployment's own `substrat.ownerGrants` (package.json). An
80
+ * install grants them to its owner, a person, so a reserved key among them is a person's grant.
81
+ */
82
+ export declare function ownerGrantSets(modules: readonly ReservedKeySource[], deployment?: readonly string[]): {
83
+ where: string;
84
+ permissions: readonly string[];
85
+ }[];
86
+ /**
87
+ * #2430: package.json `substrat.ownerGrants` as the owner-grant list `ownerGrantSets` takes, or why
88
+ * it is not one. Absent is no owner grant; anything but an array of keys is a broken declaration,
89
+ * refused rather than read as none, since what it means to grant is exactly what cannot be judged.
90
+ */
91
+ export declare function declaredOwnerGrants(value: unknown): {
92
+ grants: string[];
93
+ } | {
94
+ problem: string;
95
+ };
96
+ /** Every person-facing grant of a reserved key, and every malformed system-held role, in surface order. */
97
+ export declare function reservedKeyOffences(surface: ReservedKeySurface, reserved: {
98
+ has(key: string): boolean;
99
+ }): ReservedKeyOffence[];
100
+ /**
101
+ * One offence on one line: where, which key, and why it is refused. `reserved` names the module
102
+ * that marks the key; without it (the push boundary, which has only the registry's rows, whose
103
+ * `declaredBy` lists every declarer, marked or not) the line attributes the mark to nobody.
104
+ */
105
+ export declare function reservedKeyLine(o: ReservedKeyOffence, reserved?: ReadonlyMap<string, ReservedKey>): string;
106
+ /** The refusal's text, naming the vertical, the file, each grant, who reserved the key and the remedy. */
107
+ export declare function reservedKeyMessage(rel: string, entry: string, offences: readonly ReservedKeyOffence[], reserved: ReadonlyMap<string, ReservedKey>): string;
108
+ //# sourceMappingURL=reserved-keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reserved-keys.d.ts","sourceRoot":"","sources":["../src/reserved-keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,yEAAyE;AACzE,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,sGAAsG;AACtG,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE;QACR,EAAE,EAAE,MAAM,CAAC;QACX,WAAW,EAAE,SAAS;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,WAAW,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,QAAQ,CAAA;SAAE,EAAE,CAAC;QAChF,oFAAoF;QACpF,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;KACjC,CAAC;CACH;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,iBAAiB,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAW9F;AAED,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,2FAA2F;IAC3F,WAAW,CAAC,EAAE,IAAI,CAAC;IACnB,oEAAoE;IACpE,KAAK,CAAC,EAAE,IAAI,CAAC;CACd;AAED,0GAA0G;AAC1G,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,SAAS;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;QAAC,MAAM,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;KAAE,EAAE,CAAC;IAChG,MAAM,EAAE,SAAS;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;IAC1E,2GAA2G;IAC3G,YAAY,CAAC,EAAE,SAAS;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;CAC7E;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,SAAS,iBAAiB,EAAE,EACrC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,GAC7B;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,EAAE,CAOrD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG;IAAE,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAQ9F;AAED,2GAA2G;AAC3G,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE;IAAE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GAAG,kBAAkB,EAAE,CA4B9H;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,kBAAkB,EAAE,QAAQ,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,CAS1G;AAED,0GAA0G;AAC1G,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,SAAS,kBAAkB,EAAE,EACvC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,GACzC,MAAM,CAuBR"}
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Keys no person may hold (#2425, #2430): the one reading of `holder: 'system'` on a manifest's
3
+ * permission declaration, applied wherever a permission surface is judged. `lint:permissions`
4
+ * (`tools/reserved-keys.mts`), `buildPermissionRegistry` and so `substrat push`, the push boundary
5
+ * (`pushedPermissionRegistry`), and a scope host's role doors (the kernel's `reservedRoleRefusal`)
6
+ * all call these, so none of them can read the mark differently.
7
+ *
8
+ * A vertical's person-facing surface is its `ROLES` and its `ENTITY_GRANTS` shapes (what a
9
+ * principal is handed per entity). A module's SYSTEM principal is a different declaration:
10
+ * `manifest.schedules[].permissions` (`system:<module>`) and `manifest.peers[].permissions`
11
+ * (`vertical:<slug>`). So "a system-only key" needs no new concept in the surface: it may appear
12
+ * in the second place and never in the first. The one role that may carry one is a role declared
13
+ * `holder: 'system'` (K-55), and it may carry nothing else.
14
+ *
15
+ * This is NOT the purge-only notion of K-45 (`entity-trash.ts`): that scopes a key a system
16
+ * principal already holds to one schedule's call. It says where a system principal may USE a key,
17
+ * not that a person may not be given it.
18
+ *
19
+ * Pure and dependency-free, so `tools/` can load it from source.
20
+ */
21
+ /**
22
+ * Every key a manifest declares `holder: 'system'`.
23
+ *
24
+ * A union, not a vote: one module declaring the key reserved reserves it, whatever another module
25
+ * declaring the same key says. Otherwise a vertical could lift an engine's reservation by
26
+ * re-declaring the key, unmarked, in a module of its own.
27
+ */
28
+ export function reservedKeysOf(modules) {
29
+ const seen = new Map();
30
+ for (const m of modules) {
31
+ for (const p of m.manifest.permissions) {
32
+ if (p.holder !== 'system')
33
+ continue;
34
+ let key = seen.get(p.key);
35
+ if (!key)
36
+ seen.set(p.key, (key = { description: p.description, declaredBy: new Set() }));
37
+ key.declaredBy.add(m.manifest.id);
38
+ }
39
+ }
40
+ return new Map([...seen].map(([k, v]) => [k, { description: v.description, declaredBy: [...v.declaredBy].sort() }]));
41
+ }
42
+ /**
43
+ * #2430: the owner grants a vertical declares, as named person grants — each module manifest's
44
+ * `ownerGrants` and, when given, the deployment's own `substrat.ownerGrants` (package.json). An
45
+ * install grants them to its owner, a person, so a reserved key among them is a person's grant.
46
+ */
47
+ export function ownerGrantSets(modules, deployment) {
48
+ return [
49
+ ...modules
50
+ .filter((m) => m.manifest.ownerGrants?.length)
51
+ .map((m) => ({ where: `owner grants of ${m.manifest.id}`, permissions: m.manifest.ownerGrants })),
52
+ ...(deployment?.length ? [{ where: 'owner grants (substrat.ownerGrants)', permissions: deployment }] : []),
53
+ ];
54
+ }
55
+ /**
56
+ * #2430: package.json `substrat.ownerGrants` as the owner-grant list `ownerGrantSets` takes, or why
57
+ * it is not one. Absent is no owner grant; anything but an array of keys is a broken declaration,
58
+ * refused rather than read as none, since what it means to grant is exactly what cannot be judged.
59
+ */
60
+ export function declaredOwnerGrants(value) {
61
+ if (value === undefined)
62
+ return { grants: [] };
63
+ if (Array.isArray(value) && value.every((k) => typeof k === 'string'))
64
+ return { grants: value };
65
+ return {
66
+ problem: `declares \`substrat.ownerGrants\` as ${JSON.stringify(value)}, not an array of permission keys. List the keys a ` +
67
+ 'fresh install\'s owner is granted (`"ownerGrants": ["todo:list-create"]`), or leave the field out',
68
+ };
69
+ }
70
+ /** Every person-facing grant of a reserved key, and every malformed system-held role, in surface order. */
71
+ export function reservedKeyOffences(surface, reserved) {
72
+ const out = [];
73
+ for (const r of surface.roles) {
74
+ // A system-held role that holds no reserved key is a standing principal granting nothing
75
+ // today and widenable by editing only the permission list: refuse it as vacuous.
76
+ if (r.holder === 'system' && !r.permissions.some((p) => reserved.has(p))) {
77
+ out.push({ where: `system-held role ${r.key}`, key: '(none)', notReserved: true, empty: true });
78
+ }
79
+ for (const p of r.permissions) {
80
+ // A system-held role (K-55) is the one role shape that may carry a reserved key. It may
81
+ // carry NOTHING ELSE: a standing host principal's authority is exactly the keys reserved
82
+ // to it, so a person-grade key slipped in beside them is an offence of its own.
83
+ if (r.holder === 'system') {
84
+ if (!reserved.has(p))
85
+ out.push({ where: `system-held role ${r.key}`, key: p, notReserved: true });
86
+ }
87
+ else if (reserved.has(p))
88
+ out.push({ where: `role ${r.key}`, key: p });
89
+ }
90
+ }
91
+ for (const g of surface.grants) {
92
+ for (const p of g.permissions) {
93
+ if (reserved.has(p))
94
+ out.push({ where: `grant shape on ${g.entityType}`, key: p });
95
+ }
96
+ }
97
+ for (const g of surface.personGrants ?? []) {
98
+ for (const p of g.permissions) {
99
+ if (reserved.has(p))
100
+ out.push({ where: g.where, key: p });
101
+ }
102
+ }
103
+ return out;
104
+ }
105
+ /**
106
+ * One offence on one line: where, which key, and why it is refused. `reserved` names the module
107
+ * that marks the key; without it (the push boundary, which has only the registry's rows, whose
108
+ * `declaredBy` lists every declarer, marked or not) the line attributes the mark to nobody.
109
+ */
110
+ export function reservedKeyLine(o, reserved) {
111
+ if (o.empty)
112
+ return `${o.where} → ${o.key}: a system-held role must hold at least one key reserved to a system principal`;
113
+ if (o.notReserved) {
114
+ return `${o.where} → ${o.key}: a system-held role holds only keys reserved to a system principal (a key its module declares \`holder: 'system'\`)`;
115
+ }
116
+ const r = reserved?.get(o.key);
117
+ return r
118
+ ? `${o.where} → ${o.key}: declared \`holder: 'system'\` by ${r.declaredBy.join(', ')} (${r.description})`
119
+ : `${o.where} → ${o.key}: reserved to a system principal (\`holder: 'system'\` on its registry row)`;
120
+ }
121
+ /** The refusal's text, naming the vertical, the file, each grant, who reserved the key and the remedy. */
122
+ export function reservedKeyMessage(rel, entry, offences, reserved) {
123
+ const person = offences.filter((o) => !o.notReserved);
124
+ const system = offences.filter((o) => o.notReserved);
125
+ const lines = (os) => os.map((o) => ` ${reservedKeyLine(o, reserved)}`).join('\n');
126
+ const parts = [];
127
+ if (person.length) {
128
+ parts.push(`${rel}/${entry} grants a key reserved to a system principal to a person's role, grant shape or owner grant:\n` +
129
+ lines(person) +
130
+ `\n\n Remedy: take the key out of the role, shape or owner grant. A module's own system principal gets it\n` +
131
+ ` from a schedule's \`permissions\` (or a peer's); a platform host's standing principal gets it\n` +
132
+ ` from a role declared \`holder: 'system'\` (K-55). Never from a role a person can hold.`);
133
+ }
134
+ if (system.length) {
135
+ parts.push(`${rel}/${entry} declares a system-held role that is not made of keys reserved to a system principal:\n` +
136
+ lines(system) +
137
+ `\n\n Remedy: keep only reserved keys in a \`holder: 'system'\` role (and at least one), and move any\n` +
138
+ ` person-grade key to a role a person can hold.`);
139
+ }
140
+ return parts.join('\n\n');
141
+ }
142
+ //# sourceMappingURL=reserved-keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reserved-keys.js","sourceRoot":"","sources":["../src/reserved-keys.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,98 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * PLATFORM-PROVIDED social sign-in for a team Auth Server (#1544).
4
+ *
5
+ * The platform's social relay (`apps/social-relay`, id.substrat.net) holds one OAuth client per
6
+ * upstream — Google, GitHub, Apple — and serves one OpenID issuer per provider in front of them.
7
+ * Each Auth Server install gets its OWN client at the relay, registered by the platform, never by
8
+ * the install: the redirect URIs are built from hostnames the platform issued or bound.
9
+ *
10
+ * This module is the vocabulary the control plane's reconciler and the Auth Server share. The
11
+ * reconciler delivers one entry through the issuer's platform-gated `/internal/configure`:
12
+ *
13
+ * substrat:social-relay = { "clientId": "…", "clientSecret": "…",
14
+ * "providers": [{ "id": "github", "label": "GitHub",
15
+ * "issuer": "https://id.substrat.net/github",
16
+ * "endpoints": { … } }, …] }
17
+ *
18
+ * The value is the WHOLE set of providers the relay serves, not a delta, so re-sending it is a
19
+ * no-op and a provider that drops out is switched off. `demos/auth-server/src/social-relay.ts`
20
+ * turns it into provider rows, each named `relayProviderId(id)`, created switched off.
21
+ *
22
+ * `clientSecret` is present only when the platform has one to give — when the relay client was
23
+ * just created, or its secret just rotated. The relay stores secrets hashed and cannot hand an
24
+ * existing one back, so an ordinary re-delivery carries none and the issuer keeps the one it
25
+ * holds. An issuer that holds none answers `SOCIAL_RELAY_SECRET_NEEDED` (409), and only then
26
+ * does the platform rotate and deliver again. Rotating on every delivery instead would let two
27
+ * overlapping reconciles deliver secrets out of order and leave the older one in place.
28
+ */
29
+ export declare const SOCIAL_RELAY_CONFIG_KEY = "substrat:social-relay";
30
+ /**
31
+ * The verticals the reconciler registers at the relay. Exact slugs, never a pattern: a
32
+ * registration spends the platform's upstream client, so only the platform's own issuer
33
+ * gets one — a builder's vertical that happens to be named the same lives under
34
+ * `<tenant>/<name>` and is not matched.
35
+ */
36
+ export declare const SOCIAL_RELAY_VERTICALS: readonly string[];
37
+ /**
38
+ * The provider id an install holds a relay provider under. Suffixed, because the bare ids
39
+ * (`github`, `google`, `apple`) are Better Auth's built-in providers, which an install may
40
+ * also configure with its own credential.
41
+ */
42
+ export declare function relayProviderId(upstream: string): string;
43
+ /** The path an Auth Server answers a provider's callback on (`callbackPath` in the issuer). */
44
+ export declare function relayCallbackPath(upstream: string): string;
45
+ /** The discovery endpoints the issuer stores on a generic row; the relay states them. */
46
+ export declare const socialRelayEndpoints: z.ZodObject<{
47
+ issuer: z.ZodString;
48
+ authorization_endpoint: z.ZodString;
49
+ token_endpoint: z.ZodString;
50
+ userinfo_endpoint: z.ZodOptional<z.ZodString>;
51
+ }, z.core.$strip>;
52
+ export type SocialRelayEndpoints = z.infer<typeof socialRelayEndpoints>;
53
+ /** One provider the relay serves, as `GET /providers` lists it and the delivery carries it. */
54
+ export declare const socialRelayProvider: z.ZodObject<{
55
+ id: z.ZodString;
56
+ label: z.ZodString;
57
+ issuer: z.ZodString;
58
+ endpoints: z.ZodObject<{
59
+ issuer: z.ZodString;
60
+ authorization_endpoint: z.ZodString;
61
+ token_endpoint: z.ZodString;
62
+ userinfo_endpoint: z.ZodOptional<z.ZodString>;
63
+ }, z.core.$strip>;
64
+ }, z.core.$strip>;
65
+ export type SocialRelayProvider = z.infer<typeof socialRelayProvider>;
66
+ export declare const socialRelayProviders: z.ZodObject<{
67
+ providers: z.ZodArray<z.ZodObject<{
68
+ id: z.ZodString;
69
+ label: z.ZodString;
70
+ issuer: z.ZodString;
71
+ endpoints: z.ZodObject<{
72
+ issuer: z.ZodString;
73
+ authorization_endpoint: z.ZodString;
74
+ token_endpoint: z.ZodString;
75
+ userinfo_endpoint: z.ZodOptional<z.ZodString>;
76
+ }, z.core.$strip>;
77
+ }, z.core.$strip>>;
78
+ }, z.core.$strip>;
79
+ /** What an issuer answers (409, as the `error`) when a delivery carries no secret and it holds none. */
80
+ export declare const SOCIAL_RELAY_SECRET_NEEDED = "social_relay_secret_needed";
81
+ /** The delivered value of `substrat:social-relay`. */
82
+ export declare const socialRelayDelivery: z.ZodObject<{
83
+ clientId: z.ZodString;
84
+ clientSecret: z.ZodOptional<z.ZodString>;
85
+ providers: z.ZodArray<z.ZodObject<{
86
+ id: z.ZodString;
87
+ label: z.ZodString;
88
+ issuer: z.ZodString;
89
+ endpoints: z.ZodObject<{
90
+ issuer: z.ZodString;
91
+ authorization_endpoint: z.ZodString;
92
+ token_endpoint: z.ZodString;
93
+ userinfo_endpoint: z.ZodOptional<z.ZodString>;
94
+ }, z.core.$strip>;
95
+ }, z.core.$strip>>;
96
+ }, z.core.$strip>;
97
+ export type SocialRelayDelivery = z.infer<typeof socialRelayDelivery>;
98
+ //# sourceMappingURL=social-relay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"social-relay.d.ts","sourceRoot":"","sources":["../src/social-relay.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,uBAAuB,0BAA0B,CAAC;AAE/D;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAAS,MAAM,EAAoB,CAAC;AAEzE;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,+FAA+F;AAC/F,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,yFAAyF;AACzF,eAAO,MAAM,oBAAoB;;;;;iBAK/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE,+FAA+F;AAC/F,eAAO,MAAM,mBAAmB;;;;;;;;;;iBAK9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,eAAO,MAAM,oBAAoB;;;;;;;;;;;;iBAAgE,CAAC;AAElG,wGAAwG;AACxG,eAAO,MAAM,0BAA0B,+BAA+B,CAAC;AAEvE,sDAAsD;AACtD,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;iBAI9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
@@ -0,0 +1,72 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * PLATFORM-PROVIDED social sign-in for a team Auth Server (#1544).
4
+ *
5
+ * The platform's social relay (`apps/social-relay`, id.substrat.net) holds one OAuth client per
6
+ * upstream — Google, GitHub, Apple — and serves one OpenID issuer per provider in front of them.
7
+ * Each Auth Server install gets its OWN client at the relay, registered by the platform, never by
8
+ * the install: the redirect URIs are built from hostnames the platform issued or bound.
9
+ *
10
+ * This module is the vocabulary the control plane's reconciler and the Auth Server share. The
11
+ * reconciler delivers one entry through the issuer's platform-gated `/internal/configure`:
12
+ *
13
+ * substrat:social-relay = { "clientId": "…", "clientSecret": "…",
14
+ * "providers": [{ "id": "github", "label": "GitHub",
15
+ * "issuer": "https://id.substrat.net/github",
16
+ * "endpoints": { … } }, …] }
17
+ *
18
+ * The value is the WHOLE set of providers the relay serves, not a delta, so re-sending it is a
19
+ * no-op and a provider that drops out is switched off. `demos/auth-server/src/social-relay.ts`
20
+ * turns it into provider rows, each named `relayProviderId(id)`, created switched off.
21
+ *
22
+ * `clientSecret` is present only when the platform has one to give — when the relay client was
23
+ * just created, or its secret just rotated. The relay stores secrets hashed and cannot hand an
24
+ * existing one back, so an ordinary re-delivery carries none and the issuer keeps the one it
25
+ * holds. An issuer that holds none answers `SOCIAL_RELAY_SECRET_NEEDED` (409), and only then
26
+ * does the platform rotate and deliver again. Rotating on every delivery instead would let two
27
+ * overlapping reconciles deliver secrets out of order and leave the older one in place.
28
+ */
29
+ export const SOCIAL_RELAY_CONFIG_KEY = 'substrat:social-relay';
30
+ /**
31
+ * The verticals the reconciler registers at the relay. Exact slugs, never a pattern: a
32
+ * registration spends the platform's upstream client, so only the platform's own issuer
33
+ * gets one — a builder's vertical that happens to be named the same lives under
34
+ * `<tenant>/<name>` and is not matched.
35
+ */
36
+ export const SOCIAL_RELAY_VERTICALS = ['auth-server'];
37
+ /**
38
+ * The provider id an install holds a relay provider under. Suffixed, because the bare ids
39
+ * (`github`, `google`, `apple`) are Better Auth's built-in providers, which an install may
40
+ * also configure with its own credential.
41
+ */
42
+ export function relayProviderId(upstream) {
43
+ return `${upstream}-relay`;
44
+ }
45
+ /** The path an Auth Server answers a provider's callback on (`callbackPath` in the issuer). */
46
+ export function relayCallbackPath(upstream) {
47
+ return `/api/auth/callback/${relayProviderId(upstream)}`;
48
+ }
49
+ /** The discovery endpoints the issuer stores on a generic row; the relay states them. */
50
+ export const socialRelayEndpoints = z.object({
51
+ issuer: z.string().url(),
52
+ authorization_endpoint: z.string().url(),
53
+ token_endpoint: z.string().url(),
54
+ userinfo_endpoint: z.string().url().optional(),
55
+ });
56
+ /** One provider the relay serves, as `GET /providers` lists it and the delivery carries it. */
57
+ export const socialRelayProvider = z.object({
58
+ id: z.string().regex(/^[a-z0-9]{1,32}$/),
59
+ label: z.string().min(1).max(100),
60
+ issuer: z.string().url(),
61
+ endpoints: socialRelayEndpoints,
62
+ });
63
+ export const socialRelayProviders = z.object({ providers: z.array(socialRelayProvider).max(32) });
64
+ /** What an issuer answers (409, as the `error`) when a delivery carries no secret and it holds none. */
65
+ export const SOCIAL_RELAY_SECRET_NEEDED = 'social_relay_secret_needed';
66
+ /** The delivered value of `substrat:social-relay`. */
67
+ export const socialRelayDelivery = z.object({
68
+ clientId: z.string().min(1).max(200),
69
+ clientSecret: z.string().min(1).max(500).optional(),
70
+ providers: z.array(socialRelayProvider).max(32),
71
+ });
72
+ //# sourceMappingURL=social-relay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"social-relay.js","sourceRoot":"","sources":["../src/social-relay.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,uBAAuB,CAAC;AAE/D;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAsB,CAAC,aAAa,CAAC,CAAC;AAEzE;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,OAAO,GAAG,QAAQ,QAAQ,CAAC;AAC7B,CAAC;AAED,+FAA+F;AAC/F,MAAM,UAAU,iBAAiB,CAAC,QAAgB;IAChD,OAAO,sBAAsB,eAAe,CAAC,QAAQ,CAAC,EAAE,CAAC;AAC3D,CAAC;AAED,yFAAyF;AACzF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IACxB,sBAAsB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IACxC,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IAChC,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;CAC/C,CAAC,CAAC;AAGH,+FAA+F;AAC/F,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1C,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,kBAAkB,CAAC;IACxC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IACjC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IACxB,SAAS,EAAE,oBAAoB;CAChC,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;AAElG,wGAAwG;AACxG,MAAM,CAAC,MAAM,0BAA0B,GAAG,4BAA4B,CAAC;AAEvE,sDAAsD;AACtD,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1C,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IACpC,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;IACnD,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;CAChD,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@substrat-run/contracts",
3
- "version": "0.153.0",
3
+ "version": "0.154.0",
4
4
  "description": "Substrat kernel contract schemas — Zod is the source of truth (master plan D-22); OAS/JSON Schema are emitted artifacts",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {