@swimmesberger/elarion-jsonrpc-client-generator 0.2.3-preview.61.1 → 0.2.3-preview.62.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/cli.js CHANGED
@@ -42,6 +42,11 @@ function parseArgs(argv) {
42
42
  index += 1;
43
43
  continue;
44
44
  }
45
+ if (arg === '--session-client') {
46
+ options.sessionClientFileName = next;
47
+ index += 1;
48
+ continue;
49
+ }
45
50
  if (arg === '--source-label') {
46
51
  options.sourceLabel = next;
47
52
  index += 1;
@@ -60,6 +65,8 @@ Options:
60
65
  --types <file> TypeScript types filename (default: rpc-types.ts)
61
66
  --schemas <file> Zod schemas filename (default: rpc-schemas.ts)
62
67
  --client <file> Fetch client filename (default: rpc-client.ts)
68
+ --session-client <file> Client-capability snapshot client + OpenFeature provider (default: session-client.ts;
69
+ emitted only when the schema exposes the elarion.session operation)
63
70
  --source-label <text> Source label written into generated file headers
64
71
  `);
65
72
  }
@@ -74,11 +81,17 @@ function main() {
74
81
  typesFileName: options.typesFileName,
75
82
  schemasFileName: options.schemasFileName,
76
83
  clientFileName: options.clientFileName,
84
+ sessionClientFileName: options.sessionClientFileName,
77
85
  });
78
86
  mkdirSync(outDir, { recursive: true });
79
87
  writeFileSync(resolve(outDir, generated.typesFileName), generated.typesSource, 'utf-8');
80
88
  writeFileSync(resolve(outDir, generated.schemasFileName), generated.schemasSource, 'utf-8');
81
89
  writeFileSync(resolve(outDir, generated.clientFileName), generated.clientSource, 'utf-8');
82
- console.log(`[jsonrpc-client-generator] Generated ${generated.methodCount} RPC method types and schemas -> ${outDir}`);
90
+ let sessionNote = '';
91
+ if (generated.sessionClientFileName !== undefined && generated.sessionClientSource !== undefined) {
92
+ writeFileSync(resolve(outDir, generated.sessionClientFileName), generated.sessionClientSource, 'utf-8');
93
+ sessionNote = ` (+ ${generated.sessionClientFileName})`;
94
+ }
95
+ console.log(`[jsonrpc-client-generator] Generated ${generated.methodCount} RPC method types and schemas${sessionNote} -> ${outDir}`);
83
96
  }
84
97
  main();
package/dist/generate.js CHANGED
@@ -2,11 +2,14 @@ import { jsonSchemaToTypeScript } from './json-schema-to-ts.js';
2
2
  import { jsonSchemaToZod } from './json-schema-to-zod.js';
3
3
  import { stripNullable } from './json-schema.js';
4
4
  import { generateRpcClientSource } from './rpc-client-source.js';
5
+ import { generateSessionClientSource } from './session-client-source.js';
5
6
  const DEFAULT_GENERATED_BY = 'elarion-jsonrpc-client-generator';
6
7
  const DEFAULT_SOURCE_LABEL = 'rpc-schema.json';
7
8
  const DEFAULT_TYPES_FILE = 'rpc-types.ts';
8
9
  const DEFAULT_SCHEMAS_FILE = 'rpc-schemas.ts';
9
10
  const DEFAULT_CLIENT_FILE = 'rpc-client.ts';
11
+ const DEFAULT_SESSION_CLIENT_FILE = 'session-client.ts';
12
+ const DEFAULT_SESSION_OPERATION = 'elarion.session';
10
13
  export { UnsupportedJsonSchemaError } from './json-schema.js';
11
14
  export function generateRpcClientFiles(schema, options = {}) {
12
15
  const generatedBy = options.generatedBy ?? DEFAULT_GENERATED_BY;
@@ -68,6 +71,20 @@ export function generateRpcClientFiles(schema, options = {}) {
68
71
  methods,
69
72
  idempotentMethods,
70
73
  });
74
+ // The client-capability snapshot client + OpenFeature provider (ADR-0030) is emitted only when the host exposes
75
+ // the session operation, so a schema without it produces byte-identical output. The schema's optional
76
+ // `capabilities` vocabulary (ADR-0032) turns the emitted module/flag/permission/role names into typed constants.
77
+ const sessionOperationName = options.sessionOperationName ?? DEFAULT_SESSION_OPERATION;
78
+ const sessionClientFileName = options.sessionClientFileName ?? DEFAULT_SESSION_CLIENT_FILE;
79
+ const hasSession = Object.prototype.hasOwnProperty.call(schema.methods, sessionOperationName);
80
+ const sessionClientSource = hasSession
81
+ ? generateSessionClientSource({
82
+ generatedBy,
83
+ sourceLabel,
84
+ operationName: sessionOperationName,
85
+ capabilities: schema.capabilities,
86
+ })
87
+ : undefined;
71
88
  return {
72
89
  methodCount: methods.length,
73
90
  typesFileName,
@@ -76,6 +93,8 @@ export function generateRpcClientFiles(schema, options = {}) {
76
93
  typesSource: typesLines.join('\n'),
77
94
  schemasSource: schemasLines.join('\n'),
78
95
  clientSource,
96
+ sessionClientFileName: sessionClientSource === undefined ? undefined : sessionClientFileName,
97
+ sessionClientSource,
79
98
  };
80
99
  }
81
100
  function createContext(root, path) {
package/dist/schema.d.ts CHANGED
@@ -25,8 +25,27 @@ export interface RpcMethodSchema {
25
25
  result: JsonSchema;
26
26
  idempotent?: boolean;
27
27
  }
28
+ /** One structured permission from the schema's capability vocabulary ({resource}.{verb} plus its parts). */
29
+ export interface RpcCapabilityPermission {
30
+ permission: string;
31
+ resource: string;
32
+ verb: string;
33
+ }
34
+ /**
35
+ * The capability vocabulary block emitted by the server exporter (ADR-0032): module names with the
36
+ * `[ClientFeatures]` each exposes, the structured permission catalog, and role names. All optional — older
37
+ * schemas (or hosts without the session/authorization registrations) simply omit it.
38
+ */
39
+ export interface RpcSchemaCapabilities {
40
+ modules?: Record<string, {
41
+ features?: string[];
42
+ }>;
43
+ permissions?: RpcCapabilityPermission[];
44
+ roles?: string[];
45
+ }
28
46
  export interface RpcSchema {
29
47
  methods: Record<string, RpcMethodSchema>;
48
+ capabilities?: RpcSchemaCapabilities;
30
49
  }
31
50
  export interface GenerateRpcClientOptions {
32
51
  generatedBy?: string;
@@ -34,6 +53,10 @@ export interface GenerateRpcClientOptions {
34
53
  typesFileName?: string;
35
54
  schemasFileName?: string;
36
55
  clientFileName?: string;
56
+ /** Output filename for the client-capability snapshot client + OpenFeature provider (default `session-client.ts`). */
57
+ sessionClientFileName?: string;
58
+ /** The operation name the client-capability snapshot is served under (default `elarion.session`). */
59
+ sessionOperationName?: string;
37
60
  }
38
61
  export interface GeneratedRpcClientFiles {
39
62
  methodCount: number;
@@ -43,4 +66,10 @@ export interface GeneratedRpcClientFiles {
43
66
  typesSource: string;
44
67
  schemasSource: string;
45
68
  clientSource: string;
69
+ /**
70
+ * The client-capability snapshot client + OpenFeature provider (ADR-0020). Present only when the schema exposes
71
+ * the session operation (default `elarion.session`); otherwise both fields are `undefined`.
72
+ */
73
+ sessionClientFileName?: string;
74
+ sessionClientSource?: string;
46
75
  }
@@ -0,0 +1,22 @@
1
+ import type { RpcSchemaCapabilities } from './schema.js';
2
+ export interface GenerateSessionClientSourceOptions {
3
+ generatedBy: string;
4
+ sourceLabel: string;
5
+ /** The bus/REST operation name the session snapshot is served under (default `elarion.session`). */
6
+ operationName: string;
7
+ /** The schema's capability vocabulary (ADR-0032); absent on older schemas — types then fall back to `string`. */
8
+ capabilities?: RpcSchemaCapabilities;
9
+ }
10
+ /**
11
+ * Emits a self-contained client for the Elarion client-capability snapshot (ADR-0030): a typed `ClientSnapshot`,
12
+ * synchronous capability accessors, and an `@openfeature/web-sdk`-shaped provider that answers every key from the
13
+ * cached snapshot via the reserved `module.` / `permission.` / `role.` namespaces (a bare key is a flag/variant).
14
+ * When the schema carries the ADR-0032 capability vocabulary, the module/flag/permission/role names become typed
15
+ * constants and literal unions, so a typo in a capability check is a compile error; accessor parameters keep an
16
+ * escape hatch (`| (string & {})`) for names outside the exported vocabulary.
17
+ *
18
+ * The module has **no imports** — it neither depends on the RPC schema's generated dictionary typing nor pins
19
+ * `@openfeature/web-sdk`, so it type-checks and runs in any browser or Node target. The consumer fetches the
20
+ * snapshot through the generated RPC client (`rpc.{operation}(...)`) and hands it to the factory.
21
+ */
22
+ export declare function generateSessionClientSource(options: GenerateSessionClientSourceOptions): string;
@@ -0,0 +1,232 @@
1
+ const q = (value) => JSON.stringify(value);
2
+ /**
3
+ * Emits the typed vocabulary section: `Modules`/`Flags`/`Permissions`/`Roles` const objects (only those with
4
+ * entries) plus the `ModuleName`/`FlagName`/`PermissionName`/`RoleName` aliases the accessors reference. With no
5
+ * vocabulary every alias is `string`, so the emitted accessors keep their pre-vocabulary signatures.
6
+ */
7
+ function vocabularySection(capabilities) {
8
+ const moduleNames = Object.keys(capabilities?.modules ?? {}).sort();
9
+ const flagNames = [
10
+ ...new Set(Object.values(capabilities?.modules ?? {}).flatMap((m) => m.features ?? [])),
11
+ ].sort();
12
+ const permissions = [...(capabilities?.permissions ?? [])].sort((a, b) => a.permission < b.permission ? -1 : a.permission > b.permission ? 1 : 0);
13
+ const roles = [...(capabilities?.roles ?? [])].sort();
14
+ const lines = [];
15
+ if (moduleNames.length > 0) {
16
+ lines.push('/** Module names declared by the backend (enabled at schema-export time). */');
17
+ lines.push('export const Modules = {');
18
+ for (const name of moduleNames)
19
+ lines.push(` ${q(name)}: ${q(name)},`);
20
+ lines.push('} as const');
21
+ lines.push('export type ModuleName = (typeof Modules)[keyof typeof Modules]');
22
+ }
23
+ else {
24
+ lines.push('/** No module vocabulary in the schema — any string is accepted. */');
25
+ lines.push('export type ModuleName = string');
26
+ }
27
+ lines.push('');
28
+ if (flagNames.length > 0) {
29
+ lines.push('/** Flag/variant names the backend exposes to the client (the union of every [ClientFeatures] list). */');
30
+ lines.push('export const Flags = {');
31
+ for (const name of flagNames)
32
+ lines.push(` ${q(name)}: ${q(name)},`);
33
+ lines.push('} as const');
34
+ lines.push('export type FlagName = (typeof Flags)[keyof typeof Flags]');
35
+ }
36
+ else {
37
+ lines.push('/** No flag vocabulary in the schema — any string is accepted. */');
38
+ lines.push('export type FlagName = string');
39
+ }
40
+ lines.push('');
41
+ if (permissions.length > 0) {
42
+ const byResource = new Map();
43
+ for (const entry of permissions) {
44
+ const list = byResource.get(entry.resource) ?? [];
45
+ list.push({ verb: entry.verb, permission: entry.permission });
46
+ byResource.set(entry.resource, list);
47
+ }
48
+ lines.push('/** The declared permission catalog, nested by resource → verb (values are the composed claim strings). */');
49
+ lines.push('export const Permissions = {');
50
+ for (const [resource, entries] of [...byResource.entries()].sort(([a], [b]) => (a < b ? -1 : 1))) {
51
+ lines.push(` ${q(resource)}: {`);
52
+ for (const { verb, permission } of entries)
53
+ lines.push(` ${q(verb)}: ${q(permission)},`);
54
+ lines.push(' },');
55
+ }
56
+ lines.push('} as const');
57
+ lines.push(`export type PermissionName = ${permissions.map((p) => q(p.permission)).join(' | ')}`);
58
+ }
59
+ else {
60
+ lines.push('/** No permission vocabulary in the schema — any string is accepted. */');
61
+ lines.push('export type PermissionName = string');
62
+ }
63
+ lines.push('');
64
+ if (roles.length > 0) {
65
+ lines.push('/** Role names declared by the backend ([RequireRole] across enabled modules). */');
66
+ lines.push('export const Roles = {');
67
+ for (const name of roles)
68
+ lines.push(` ${q(name)}: ${q(name)},`);
69
+ lines.push('} as const');
70
+ lines.push('export type RoleName = (typeof Roles)[keyof typeof Roles]');
71
+ }
72
+ else {
73
+ lines.push('/** No role vocabulary in the schema — any string is accepted. */');
74
+ lines.push('export type RoleName = string');
75
+ }
76
+ return lines.join('\n');
77
+ }
78
+ /**
79
+ * Emits a self-contained client for the Elarion client-capability snapshot (ADR-0030): a typed `ClientSnapshot`,
80
+ * synchronous capability accessors, and an `@openfeature/web-sdk`-shaped provider that answers every key from the
81
+ * cached snapshot via the reserved `module.` / `permission.` / `role.` namespaces (a bare key is a flag/variant).
82
+ * When the schema carries the ADR-0032 capability vocabulary, the module/flag/permission/role names become typed
83
+ * constants and literal unions, so a typo in a capability check is a compile error; accessor parameters keep an
84
+ * escape hatch (`| (string & {})`) for names outside the exported vocabulary.
85
+ *
86
+ * The module has **no imports** — it neither depends on the RPC schema's generated dictionary typing nor pins
87
+ * `@openfeature/web-sdk`, so it type-checks and runs in any browser or Node target. The consumer fetches the
88
+ * snapshot through the generated RPC client (`rpc.{operation}(...)`) and hands it to the factory.
89
+ */
90
+ export function generateSessionClientSource(options) {
91
+ const { generatedBy, sourceLabel, operationName, capabilities } = options;
92
+ return `// Auto-generated by ${generatedBy} — DO NOT EDIT
93
+ // Source: ${sourceLabel}
94
+ //
95
+ // Client-capability snapshot (operation: ${JSON.stringify(operationName)}). Fetch it with the generated RPC client
96
+ // and pass it to createSessionCapabilities / createElarionOpenFeatureProvider.
97
+
98
+ /** The current user's identity and raw grants, as projected into the snapshot. */
99
+ export interface ClientSnapshotUser {
100
+ readonly id: string
101
+ readonly email?: string | null
102
+ readonly isAuthenticated: boolean
103
+ readonly roles: readonly string[]
104
+ readonly permissions: readonly string[]
105
+ }
106
+
107
+ /** The client-capability snapshot returned by the ${JSON.stringify(operationName)} operation. */
108
+ export interface ClientSnapshot {
109
+ readonly user: ClientSnapshotUser
110
+ readonly modules: Readonly<Record<string, boolean>>
111
+ readonly flags: Readonly<Record<string, boolean>>
112
+ readonly variants: Readonly<Record<string, string>>
113
+ }
114
+
115
+ ${vocabularySection(capabilities)}
116
+
117
+ /** The reserved OpenFeature key namespaces and their typed builders. */
118
+ export const Keys = {
119
+ module: (name: ModuleName | (string & {})): string => \`module.\${name}\`,
120
+ permission: (permission: PermissionName | (string & {})): string => \`permission.\${permission}\`,
121
+ role: (role: RoleName | (string & {})): string => \`role.\${role}\`,
122
+ } as const
123
+
124
+ /** Synchronous, typed accessors over a cached snapshot. */
125
+ export class SessionCapabilities {
126
+ // An explicit field, not a constructor parameter property — the emitted code must stay valid under
127
+ // 'erasableSyntaxOnly' (parameter properties are non-erasable TypeScript).
128
+ private readonly snapshot: ClientSnapshot
129
+
130
+ constructor(snapshot: ClientSnapshot) {
131
+ this.snapshot = snapshot
132
+ }
133
+
134
+ get user(): ClientSnapshotUser {
135
+ return this.snapshot.user
136
+ }
137
+
138
+ isModuleEnabled(name: ModuleName | (string & {})): boolean {
139
+ return this.snapshot.modules[name] ?? false
140
+ }
141
+
142
+ hasRole(role: RoleName | (string & {})): boolean {
143
+ return this.snapshot.user.roles.includes(role)
144
+ }
145
+
146
+ hasPermission(permission: PermissionName | (string & {})): boolean {
147
+ return this.snapshot.user.permissions.includes(permission)
148
+ }
149
+
150
+ isFlagEnabled(name: FlagName | (string & {})): boolean {
151
+ return this.snapshot.flags[name] ?? false
152
+ }
153
+
154
+ getVariant(name: FlagName | (string & {})): string | undefined {
155
+ return this.snapshot.variants[name]
156
+ }
157
+
158
+ /** Resolves a reserved-namespace boolean key (the OpenFeature boolean read). */
159
+ resolveBoolean(key: string): boolean {
160
+ if (key.startsWith('module.')) return this.isModuleEnabled(key.slice('module.'.length))
161
+ if (key.startsWith('permission.')) return this.hasPermission(key.slice('permission.'.length))
162
+ if (key.startsWith('role.')) return this.hasRole(key.slice('role.'.length))
163
+ return this.isFlagEnabled(key)
164
+ }
165
+ }
166
+
167
+ export function createSessionCapabilities(snapshot: ClientSnapshot): SessionCapabilities {
168
+ return new SessionCapabilities(snapshot)
169
+ }
170
+
171
+ /** A single flag resolution result (structurally compatible with OpenFeature's ResolutionDetails). */
172
+ export interface ResolutionDetails<T> {
173
+ readonly value: T
174
+ readonly variant?: string
175
+ readonly reason: string
176
+ }
177
+
178
+ /**
179
+ * The subset of the \`@openfeature/web-sdk\` Provider surface this snapshot provider implements. Register it with
180
+ * \`OpenFeature.setProvider(provider as unknown as Provider)\` — the static-context (web) paradigm reads
181
+ * synchronously, so module enablement (deployment-scoped, context-invariant) rides the same snapshot.
182
+ */
183
+ export interface ElarionWebProvider {
184
+ readonly metadata: { readonly name: string }
185
+ readonly runsOn: 'client'
186
+ resolveBooleanEvaluation(flagKey: string, defaultValue: boolean): ResolutionDetails<boolean>
187
+ resolveStringEvaluation(flagKey: string, defaultValue: string): ResolutionDetails<string>
188
+ resolveNumberEvaluation(flagKey: string, defaultValue: number): ResolutionDetails<number>
189
+ resolveObjectEvaluation<T>(flagKey: string, defaultValue: T): ResolutionDetails<T>
190
+ }
191
+
192
+ const STATIC = 'STATIC'
193
+ const DEFAULT = 'DEFAULT'
194
+
195
+ /**
196
+ * Builds an OpenFeature web-SDK provider hydrated from one session snapshot. Every key resolves from the cache:
197
+ * \`module.{name}\` / \`permission.{p}\` / \`role.{r}\` are booleans, a bare key is a flag (boolean) or a variant
198
+ * (string), and anything else falls back to the default. No network calls — refresh by building a new provider
199
+ * from a freshly fetched snapshot (e.g. on \`onContextChange\`).
200
+ */
201
+ export function createElarionOpenFeatureProvider(snapshot: ClientSnapshot): ElarionWebProvider {
202
+ const capabilities = new SessionCapabilities(snapshot)
203
+ return {
204
+ metadata: { name: 'elarion-session' },
205
+ runsOn: 'client',
206
+ resolveBooleanEvaluation(flagKey, defaultValue) {
207
+ if (
208
+ flagKey.startsWith('module.') ||
209
+ flagKey.startsWith('permission.') ||
210
+ flagKey.startsWith('role.')
211
+ ) {
212
+ return { value: capabilities.resolveBoolean(flagKey), reason: STATIC }
213
+ }
214
+ const flag = snapshot.flags[flagKey]
215
+ return flag === undefined ? { value: defaultValue, reason: DEFAULT } : { value: flag, reason: STATIC }
216
+ },
217
+ resolveStringEvaluation(flagKey, defaultValue) {
218
+ const variant = capabilities.getVariant(flagKey)
219
+ return variant === undefined
220
+ ? { value: defaultValue, reason: DEFAULT }
221
+ : { value: variant, variant, reason: STATIC }
222
+ },
223
+ resolveNumberEvaluation(_flagKey, defaultValue) {
224
+ return { value: defaultValue, reason: DEFAULT }
225
+ },
226
+ resolveObjectEvaluation(_flagKey, defaultValue) {
227
+ return { value: defaultValue, reason: DEFAULT }
228
+ },
229
+ }
230
+ }
231
+ `;
232
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swimmesberger/elarion-jsonrpc-client-generator",
3
- "version": "0.2.3-preview.61.1",
3
+ "version": "0.2.3-preview.62.1",
4
4
  "description": "Generate TypeScript RPC contracts, Zod schemas, and a fetch client from Elarion JSON-RPC schema exports.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",