@fabrika/auth 0.9.0-dev.1 → 0.9.0-dev.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabrika/auth",
3
- "version": "0.9.0-dev.1",
3
+ "version": "0.9.0-dev.2",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,12 +12,13 @@
12
12
  },
13
13
  "files": [
14
14
  "src",
15
- "!src/**/__tests__/**"
15
+ "!src/**/__tests__/**",
16
+ "types"
16
17
  ],
17
18
  "exports": {
18
19
  ".": {
19
20
  "bun": "./src/index.ts",
20
- "types": "./src/index.ts",
21
+ "types": "./types/src/index.d.ts",
21
22
  "default": "./src/index.ts"
22
23
  }
23
24
  },
@@ -27,8 +28,8 @@
27
28
  },
28
29
  "dependencies": {
29
30
  "zod": "^3.25.76",
30
- "@fabrika/auth-core": "0.9.0-dev.1",
31
- "@fabrika/jwks": "0.9.0-dev.1",
31
+ "@fabrika/auth-core": "0.9.0-dev.2",
32
+ "@fabrika/jwks": "0.9.0-dev.2",
32
33
  "jose": "^5.9.0"
33
34
  }
34
35
  }
@@ -0,0 +1,609 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * An app's authz vocabulary, as an app declares it and IAM reconciles it. Every role permission must
4
+ * name an action the same body declares — IAM's own rule, checked wherever the vocabulary is read. A
5
+ * contract application's roles list exact actions, and a membership role names a declared scope. The
6
+ * membership and link declarations name only scopes, roles and exact actions of the same body.
7
+ */
8
+ export declare const appSchemaSchema: z.ZodEffects<z.ZodObject<{
9
+ scopes: z.ZodArray<z.ZodObject<{
10
+ type: z.ZodString;
11
+ label: z.ZodOptional<z.ZodString>;
12
+ in: z.ZodOptional<z.ZodString>;
13
+ creator: z.ZodOptional<z.ZodString>;
14
+ members: z.ZodOptional<z.ZodObject<{
15
+ manage: z.ZodString;
16
+ list: z.ZodString;
17
+ }, "strip", z.ZodTypeAny, {
18
+ manage: string;
19
+ list: string;
20
+ }, {
21
+ manage: string;
22
+ list: string;
23
+ }>>;
24
+ directory: z.ZodOptional<z.ZodLiteral<"labels">>;
25
+ lookups: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
26
+ }, "strip", z.ZodTypeAny, {
27
+ type: string;
28
+ in?: string | undefined;
29
+ members?: {
30
+ manage: string;
31
+ list: string;
32
+ } | undefined;
33
+ directory?: "labels" | undefined;
34
+ label?: string | undefined;
35
+ creator?: string | undefined;
36
+ lookups?: string[] | undefined;
37
+ }, {
38
+ type: string;
39
+ in?: string | undefined;
40
+ members?: {
41
+ manage: string;
42
+ list: string;
43
+ } | undefined;
44
+ directory?: "labels" | undefined;
45
+ label?: string | undefined;
46
+ creator?: string | undefined;
47
+ lookups?: string[] | undefined;
48
+ }>, "many">;
49
+ actions: z.ZodArray<z.ZodObject<{
50
+ action: z.ZodString;
51
+ description: z.ZodOptional<z.ZodString>;
52
+ }, "strip", z.ZodTypeAny, {
53
+ action: string;
54
+ description?: string | undefined;
55
+ }, {
56
+ action: string;
57
+ description?: string | undefined;
58
+ }>, "many">;
59
+ roles: z.ZodRecord<z.ZodEffects<z.ZodString, string, string>, z.ZodObject<{
60
+ name: z.ZodString;
61
+ description: z.ZodOptional<z.ZodString>;
62
+ permissions: z.ZodArray<z.ZodString, "many">;
63
+ member: z.ZodOptional<z.ZodObject<{
64
+ scope: z.ZodString;
65
+ assignableBy: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
66
+ }, "strip", z.ZodTypeAny, {
67
+ scope: string;
68
+ assignableBy?: string[] | undefined;
69
+ }, {
70
+ scope: string;
71
+ assignableBy?: string[] | undefined;
72
+ }>>;
73
+ external: z.ZodOptional<z.ZodLiteral<true>>;
74
+ }, "strip", z.ZodTypeAny, {
75
+ name: string;
76
+ permissions: string[];
77
+ member?: {
78
+ scope: string;
79
+ assignableBy?: string[] | undefined;
80
+ } | undefined;
81
+ external?: true | undefined;
82
+ description?: string | undefined;
83
+ }, {
84
+ name: string;
85
+ permissions: string[];
86
+ member?: {
87
+ scope: string;
88
+ assignableBy?: string[] | undefined;
89
+ } | undefined;
90
+ external?: true | undefined;
91
+ description?: string | undefined;
92
+ }>>;
93
+ contract: z.ZodOptional<z.ZodBoolean>;
94
+ links: z.ZodOptional<z.ZodRecord<z.ZodEffects<z.ZodString, string, string>, z.ZodObject<{
95
+ dimension: z.ZodString;
96
+ grants: z.ZodArray<z.ZodString, "many">;
97
+ issuedBy: z.ZodString;
98
+ maxDays: z.ZodNumber;
99
+ }, "strip", z.ZodTypeAny, {
100
+ dimension: string;
101
+ grants: string[];
102
+ issuedBy: string;
103
+ maxDays: number;
104
+ }, {
105
+ dimension: string;
106
+ grants: string[];
107
+ issuedBy: string;
108
+ maxDays: number;
109
+ }>>>;
110
+ delegations: z.ZodOptional<z.ZodRecord<z.ZodEffects<z.ZodString, string, string>, z.ZodObject<{
111
+ action: z.ZodString;
112
+ dimension: z.ZodString;
113
+ purpose: z.ZodEnum<["members", "creates"]>;
114
+ }, "strip", z.ZodTypeAny, {
115
+ action: string;
116
+ dimension: string;
117
+ purpose: "creates" | "members";
118
+ }, {
119
+ action: string;
120
+ dimension: string;
121
+ purpose: "creates" | "members";
122
+ }>>>;
123
+ workloads: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
124
+ kind: z.ZodLiteral<"job">;
125
+ name: z.ZodString;
126
+ actions: z.ZodArray<z.ZodString, "many">;
127
+ issues: z.ZodOptional<z.ZodString>;
128
+ }, "strip", z.ZodTypeAny, {
129
+ name: string;
130
+ actions: string[];
131
+ kind: "job";
132
+ issues?: string | undefined;
133
+ }, {
134
+ name: string;
135
+ actions: string[];
136
+ kind: "job";
137
+ issues?: string | undefined;
138
+ }>, z.ZodObject<{
139
+ kind: z.ZodLiteral<"webhook">;
140
+ name: z.ZodString;
141
+ actions: z.ZodArray<z.ZodString, "many">;
142
+ }, "strip", z.ZodTypeAny, {
143
+ name: string;
144
+ actions: string[];
145
+ kind: "webhook";
146
+ }, {
147
+ name: string;
148
+ actions: string[];
149
+ kind: "webhook";
150
+ }>, z.ZodObject<{
151
+ kind: z.ZodLiteral<"public">;
152
+ name: z.ZodEffects<z.ZodString, string, string>;
153
+ actions: z.ZodArray<z.ZodString, "many">;
154
+ }, "strip", z.ZodTypeAny, {
155
+ name: string;
156
+ actions: string[];
157
+ kind: "public";
158
+ }, {
159
+ name: string;
160
+ actions: string[];
161
+ kind: "public";
162
+ }>, z.ZodObject<{
163
+ kind: z.ZodLiteral<"app">;
164
+ name: z.ZodString;
165
+ actions: z.ZodArray<z.ZodString, "many">;
166
+ }, "strip", z.ZodTypeAny, {
167
+ name: string;
168
+ actions: string[];
169
+ kind: "app";
170
+ }, {
171
+ name: string;
172
+ actions: string[];
173
+ kind: "app";
174
+ }>]>, "many">>;
175
+ locale: z.ZodOptional<z.ZodEnum<["en", "cs"]>>;
176
+ displayName: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
177
+ mail: z.ZodOptional<z.ZodObject<{
178
+ invitation: z.ZodOptional<z.ZodEffects<z.ZodRecord<z.ZodEnum<["en", "cs"]>, z.ZodObject<{
179
+ subject: z.ZodString;
180
+ text: z.ZodString;
181
+ html: z.ZodOptional<z.ZodString>;
182
+ }, "strict", z.ZodTypeAny, {
183
+ subject: string;
184
+ text: string;
185
+ html?: string | undefined;
186
+ }, {
187
+ subject: string;
188
+ text: string;
189
+ html?: string | undefined;
190
+ }>>, Partial<Record<"en" | "cs", {
191
+ subject: string;
192
+ text: string;
193
+ html?: string | undefined;
194
+ }>>, Partial<Record<"en" | "cs", {
195
+ subject: string;
196
+ text: string;
197
+ html?: string | undefined;
198
+ }>>>>;
199
+ enrollment: z.ZodOptional<z.ZodEffects<z.ZodRecord<z.ZodEnum<["en", "cs"]>, z.ZodObject<{
200
+ subject: z.ZodString;
201
+ text: z.ZodString;
202
+ html: z.ZodOptional<z.ZodString>;
203
+ }, "strict", z.ZodTypeAny, {
204
+ subject: string;
205
+ text: string;
206
+ html?: string | undefined;
207
+ }, {
208
+ subject: string;
209
+ text: string;
210
+ html?: string | undefined;
211
+ }>>, Partial<Record<"en" | "cs", {
212
+ subject: string;
213
+ text: string;
214
+ html?: string | undefined;
215
+ }>>, Partial<Record<"en" | "cs", {
216
+ subject: string;
217
+ text: string;
218
+ html?: string | undefined;
219
+ }>>>>;
220
+ reset: z.ZodOptional<z.ZodEffects<z.ZodRecord<z.ZodEnum<["en", "cs"]>, z.ZodObject<{
221
+ subject: z.ZodString;
222
+ text: z.ZodString;
223
+ html: z.ZodOptional<z.ZodString>;
224
+ }, "strict", z.ZodTypeAny, {
225
+ subject: string;
226
+ text: string;
227
+ html?: string | undefined;
228
+ }, {
229
+ subject: string;
230
+ text: string;
231
+ html?: string | undefined;
232
+ }>>, Partial<Record<"en" | "cs", {
233
+ subject: string;
234
+ text: string;
235
+ html?: string | undefined;
236
+ }>>, Partial<Record<"en" | "cs", {
237
+ subject: string;
238
+ text: string;
239
+ html?: string | undefined;
240
+ }>>>>;
241
+ }, "strict", z.ZodTypeAny, {
242
+ invitation?: Partial<Record<"en" | "cs", {
243
+ subject: string;
244
+ text: string;
245
+ html?: string | undefined;
246
+ }>> | undefined;
247
+ enrollment?: Partial<Record<"en" | "cs", {
248
+ subject: string;
249
+ text: string;
250
+ html?: string | undefined;
251
+ }>> | undefined;
252
+ reset?: Partial<Record<"en" | "cs", {
253
+ subject: string;
254
+ text: string;
255
+ html?: string | undefined;
256
+ }>> | undefined;
257
+ }, {
258
+ invitation?: Partial<Record<"en" | "cs", {
259
+ subject: string;
260
+ text: string;
261
+ html?: string | undefined;
262
+ }>> | undefined;
263
+ enrollment?: Partial<Record<"en" | "cs", {
264
+ subject: string;
265
+ text: string;
266
+ html?: string | undefined;
267
+ }>> | undefined;
268
+ reset?: Partial<Record<"en" | "cs", {
269
+ subject: string;
270
+ text: string;
271
+ html?: string | undefined;
272
+ }>> | undefined;
273
+ }>>;
274
+ members: z.ZodOptional<z.ZodObject<{
275
+ manage: z.ZodString;
276
+ list: z.ZodString;
277
+ }, "strip", z.ZodTypeAny, {
278
+ manage: string;
279
+ list: string;
280
+ }, {
281
+ manage: string;
282
+ list: string;
283
+ }>>;
284
+ directory: z.ZodOptional<z.ZodLiteral<"labels">>;
285
+ }, "strip", z.ZodTypeAny, {
286
+ actions: {
287
+ action: string;
288
+ description?: string | undefined;
289
+ }[];
290
+ roles: Record<string, {
291
+ name: string;
292
+ permissions: string[];
293
+ member?: {
294
+ scope: string;
295
+ assignableBy?: string[] | undefined;
296
+ } | undefined;
297
+ external?: true | undefined;
298
+ description?: string | undefined;
299
+ }>;
300
+ scopes: {
301
+ type: string;
302
+ in?: string | undefined;
303
+ members?: {
304
+ manage: string;
305
+ list: string;
306
+ } | undefined;
307
+ directory?: "labels" | undefined;
308
+ label?: string | undefined;
309
+ creator?: string | undefined;
310
+ lookups?: string[] | undefined;
311
+ }[];
312
+ displayName?: string | undefined;
313
+ locale?: "en" | "cs" | undefined;
314
+ mail?: {
315
+ invitation?: Partial<Record<"en" | "cs", {
316
+ subject: string;
317
+ text: string;
318
+ html?: string | undefined;
319
+ }>> | undefined;
320
+ enrollment?: Partial<Record<"en" | "cs", {
321
+ subject: string;
322
+ text: string;
323
+ html?: string | undefined;
324
+ }>> | undefined;
325
+ reset?: Partial<Record<"en" | "cs", {
326
+ subject: string;
327
+ text: string;
328
+ html?: string | undefined;
329
+ }>> | undefined;
330
+ } | undefined;
331
+ members?: {
332
+ manage: string;
333
+ list: string;
334
+ } | undefined;
335
+ directory?: "labels" | undefined;
336
+ contract?: boolean | undefined;
337
+ links?: Record<string, {
338
+ dimension: string;
339
+ grants: string[];
340
+ issuedBy: string;
341
+ maxDays: number;
342
+ }> | undefined;
343
+ delegations?: Record<string, {
344
+ action: string;
345
+ dimension: string;
346
+ purpose: "creates" | "members";
347
+ }> | undefined;
348
+ workloads?: ({
349
+ name: string;
350
+ actions: string[];
351
+ kind: "job";
352
+ issues?: string | undefined;
353
+ } | {
354
+ name: string;
355
+ actions: string[];
356
+ kind: "webhook";
357
+ } | {
358
+ name: string;
359
+ actions: string[];
360
+ kind: "public";
361
+ } | {
362
+ name: string;
363
+ actions: string[];
364
+ kind: "app";
365
+ })[] | undefined;
366
+ }, {
367
+ actions: {
368
+ action: string;
369
+ description?: string | undefined;
370
+ }[];
371
+ roles: Record<string, {
372
+ name: string;
373
+ permissions: string[];
374
+ member?: {
375
+ scope: string;
376
+ assignableBy?: string[] | undefined;
377
+ } | undefined;
378
+ external?: true | undefined;
379
+ description?: string | undefined;
380
+ }>;
381
+ scopes: {
382
+ type: string;
383
+ in?: string | undefined;
384
+ members?: {
385
+ manage: string;
386
+ list: string;
387
+ } | undefined;
388
+ directory?: "labels" | undefined;
389
+ label?: string | undefined;
390
+ creator?: string | undefined;
391
+ lookups?: string[] | undefined;
392
+ }[];
393
+ displayName?: string | undefined;
394
+ locale?: "en" | "cs" | undefined;
395
+ mail?: {
396
+ invitation?: Partial<Record<"en" | "cs", {
397
+ subject: string;
398
+ text: string;
399
+ html?: string | undefined;
400
+ }>> | undefined;
401
+ enrollment?: Partial<Record<"en" | "cs", {
402
+ subject: string;
403
+ text: string;
404
+ html?: string | undefined;
405
+ }>> | undefined;
406
+ reset?: Partial<Record<"en" | "cs", {
407
+ subject: string;
408
+ text: string;
409
+ html?: string | undefined;
410
+ }>> | undefined;
411
+ } | undefined;
412
+ members?: {
413
+ manage: string;
414
+ list: string;
415
+ } | undefined;
416
+ directory?: "labels" | undefined;
417
+ contract?: boolean | undefined;
418
+ links?: Record<string, {
419
+ dimension: string;
420
+ grants: string[];
421
+ issuedBy: string;
422
+ maxDays: number;
423
+ }> | undefined;
424
+ delegations?: Record<string, {
425
+ action: string;
426
+ dimension: string;
427
+ purpose: "creates" | "members";
428
+ }> | undefined;
429
+ workloads?: ({
430
+ name: string;
431
+ actions: string[];
432
+ kind: "job";
433
+ issues?: string | undefined;
434
+ } | {
435
+ name: string;
436
+ actions: string[];
437
+ kind: "webhook";
438
+ } | {
439
+ name: string;
440
+ actions: string[];
441
+ kind: "public";
442
+ } | {
443
+ name: string;
444
+ actions: string[];
445
+ kind: "app";
446
+ })[] | undefined;
447
+ }>, {
448
+ actions: {
449
+ action: string;
450
+ description?: string | undefined;
451
+ }[];
452
+ roles: Record<string, {
453
+ name: string;
454
+ permissions: string[];
455
+ member?: {
456
+ scope: string;
457
+ assignableBy?: string[] | undefined;
458
+ } | undefined;
459
+ external?: true | undefined;
460
+ description?: string | undefined;
461
+ }>;
462
+ scopes: {
463
+ type: string;
464
+ in?: string | undefined;
465
+ members?: {
466
+ manage: string;
467
+ list: string;
468
+ } | undefined;
469
+ directory?: "labels" | undefined;
470
+ label?: string | undefined;
471
+ creator?: string | undefined;
472
+ lookups?: string[] | undefined;
473
+ }[];
474
+ displayName?: string | undefined;
475
+ locale?: "en" | "cs" | undefined;
476
+ mail?: {
477
+ invitation?: Partial<Record<"en" | "cs", {
478
+ subject: string;
479
+ text: string;
480
+ html?: string | undefined;
481
+ }>> | undefined;
482
+ enrollment?: Partial<Record<"en" | "cs", {
483
+ subject: string;
484
+ text: string;
485
+ html?: string | undefined;
486
+ }>> | undefined;
487
+ reset?: Partial<Record<"en" | "cs", {
488
+ subject: string;
489
+ text: string;
490
+ html?: string | undefined;
491
+ }>> | undefined;
492
+ } | undefined;
493
+ members?: {
494
+ manage: string;
495
+ list: string;
496
+ } | undefined;
497
+ directory?: "labels" | undefined;
498
+ contract?: boolean | undefined;
499
+ links?: Record<string, {
500
+ dimension: string;
501
+ grants: string[];
502
+ issuedBy: string;
503
+ maxDays: number;
504
+ }> | undefined;
505
+ delegations?: Record<string, {
506
+ action: string;
507
+ dimension: string;
508
+ purpose: "creates" | "members";
509
+ }> | undefined;
510
+ workloads?: ({
511
+ name: string;
512
+ actions: string[];
513
+ kind: "job";
514
+ issues?: string | undefined;
515
+ } | {
516
+ name: string;
517
+ actions: string[];
518
+ kind: "webhook";
519
+ } | {
520
+ name: string;
521
+ actions: string[];
522
+ kind: "public";
523
+ } | {
524
+ name: string;
525
+ actions: string[];
526
+ kind: "app";
527
+ })[] | undefined;
528
+ }, {
529
+ actions: {
530
+ action: string;
531
+ description?: string | undefined;
532
+ }[];
533
+ roles: Record<string, {
534
+ name: string;
535
+ permissions: string[];
536
+ member?: {
537
+ scope: string;
538
+ assignableBy?: string[] | undefined;
539
+ } | undefined;
540
+ external?: true | undefined;
541
+ description?: string | undefined;
542
+ }>;
543
+ scopes: {
544
+ type: string;
545
+ in?: string | undefined;
546
+ members?: {
547
+ manage: string;
548
+ list: string;
549
+ } | undefined;
550
+ directory?: "labels" | undefined;
551
+ label?: string | undefined;
552
+ creator?: string | undefined;
553
+ lookups?: string[] | undefined;
554
+ }[];
555
+ displayName?: string | undefined;
556
+ locale?: "en" | "cs" | undefined;
557
+ mail?: {
558
+ invitation?: Partial<Record<"en" | "cs", {
559
+ subject: string;
560
+ text: string;
561
+ html?: string | undefined;
562
+ }>> | undefined;
563
+ enrollment?: Partial<Record<"en" | "cs", {
564
+ subject: string;
565
+ text: string;
566
+ html?: string | undefined;
567
+ }>> | undefined;
568
+ reset?: Partial<Record<"en" | "cs", {
569
+ subject: string;
570
+ text: string;
571
+ html?: string | undefined;
572
+ }>> | undefined;
573
+ } | undefined;
574
+ members?: {
575
+ manage: string;
576
+ list: string;
577
+ } | undefined;
578
+ directory?: "labels" | undefined;
579
+ contract?: boolean | undefined;
580
+ links?: Record<string, {
581
+ dimension: string;
582
+ grants: string[];
583
+ issuedBy: string;
584
+ maxDays: number;
585
+ }> | undefined;
586
+ delegations?: Record<string, {
587
+ action: string;
588
+ dimension: string;
589
+ purpose: "creates" | "members";
590
+ }> | undefined;
591
+ workloads?: ({
592
+ name: string;
593
+ actions: string[];
594
+ kind: "job";
595
+ issues?: string | undefined;
596
+ } | {
597
+ name: string;
598
+ actions: string[];
599
+ kind: "webhook";
600
+ } | {
601
+ name: string;
602
+ actions: string[];
603
+ kind: "public";
604
+ } | {
605
+ name: string;
606
+ actions: string[];
607
+ kind: "app";
608
+ })[] | undefined;
609
+ }>;
@@ -0,0 +1,28 @@
1
+ import type { AccessTokenClaims } from '@fabrika/auth-core';
2
+ import type { AuthContext, IamBinding, ListPrincipalsFailure, PrincipalList } from './types';
3
+ /**
4
+ * Build an `AuthContext` from a verified access token and its claims — the seam every authentication
5
+ * path uses (the proxy-injected token). The SDK has verified the token LOCALLY; this wraps its claims in the
6
+ * same `permits`-backed context the RPC path returns,
7
+ * so `can()` / `scopedTo()` / `audit()` behave identically. A token with no `ptype` is anonymous
8
+ * (`principal: null`).
9
+ */
10
+ export declare function buildAuthContext(binding: IamBinding, token: string, claims: AccessTokenClaims, requestId: string): AuthContext;
11
+ /**
12
+ * Thin, app-facing wrapper over the IAM Worker service binding. Bakes the caller `app` id
13
+ * into the constructor so app code can never forget or mistype it. Depends ONLY on the
14
+ * `IamRpc` contract from `@fabrika/auth-core` — never on the Worker — which is what keeps the
15
+ * SDK worker-independent (the binding is the deployed Worker, reached at runtime).
16
+ */
17
+ export declare class IamClient {
18
+ private readonly binding;
19
+ private readonly appId;
20
+ constructor(binding: IamBinding, appId: string);
21
+ /**
22
+ * List the app's people directory — the USER principals who can access this app. Forwards the
23
+ * CALLER's credential; the IAM Worker resolves the caller and scopes the roster to the
24
+ * verified app (you only ever see your own app's people). For an assignee picker / actor
25
+ * label list. Authorized for any member with a permission on the app; a zero-grant user → 403.
26
+ */
27
+ listPrincipals(req: Request): Promise<PrincipalList | ListPrincipalsFailure>;
28
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Typed request-time errors + a small authorization helper.
3
+ *
4
+ * These are the shared structural HTTP-error contract `@fabrika/app` maps without any
5
+ * cross-package `instanceof`: each error exposes `{ httpStatus, type, message }` plus an optional
6
+ * `issues` (validation detail). A mapper reads those fields structurally — it never imports these
7
+ * classes — so these shapes and the framework reader are the single contract. They live in
8
+ * `@fabrika/auth` because `requirePermission` consumes an `AuthContext` (a client type).
9
+ *
10
+ * There is deliberately no login-bounce error here: since ADR-0022 the proxy 302s an unauthenticated
11
+ * browser, so an app never has to produce a login URL of its own.
12
+ */
13
+ import type { Scope } from '@fabrika/auth-core';
14
+ import type { AuthContext } from './types';
15
+ /**
16
+ * The structural HTTP-error shape. Anything thrown through a request pipeline that satisfies this can be
17
+ * mapped to a Response without an `instanceof` check: `httpStatus` → the status, `type` → the error
18
+ * envelope's `type`, `message` → its `message`, `issues` → optional detail.
19
+ */
20
+ export interface HttpError {
21
+ readonly httpStatus: number;
22
+ readonly type: string;
23
+ readonly message: string;
24
+ readonly issues?: unknown;
25
+ }
26
+ /** A caller is unauthenticated. `type: 'auth'`, status 401. */
27
+ export declare class UnauthenticatedError extends Error implements HttpError {
28
+ readonly httpStatus = 401;
29
+ readonly type = "auth";
30
+ constructor(message?: string);
31
+ }
32
+ /** A resolved caller lacks the required permission. `type: 'forbidden'`, status 403. */
33
+ export declare class ForbiddenError extends Error implements HttpError {
34
+ readonly httpStatus = 403;
35
+ readonly type = "forbidden";
36
+ constructor(message?: string);
37
+ }
38
+ /**
39
+ * Assert the caller may perform `action` (optionally within `scope`). Throws `ForbiddenError` when
40
+ * `auth.can(action, scope)` is false; otherwise returns. The thrown error satisfies the structural
41
+ * `HttpError` contract, so a pipeline maps it to a 403 without importing this module.
42
+ */
43
+ export declare function requirePermission(auth: AuthContext, action: string, scope?: Scope): void;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * `createIam` — the single request-time entry point apps use instead of hand-rolling auth.
3
+ *
4
+ * Since [ADR-0022](../../../docs/decisions/0022-the-proxy-is-the-only-enforcement-point.md) the PROXY is the only
5
+ * thing that enforces: it selects the endpoint, resolves the credential, and injects a verified
6
+ * access token as `PROXY_TOKEN_HEADER`. What is left for an app is small and fixed:
7
+ * - `authenticate(request)` — read that header and verify it LOCALLY against IAM's published JWKS
8
+ * (signature, `iss`, `aud`, `exp`), then build an `AuthContext`. The header is never trusted blindly.
9
+ * - `listPrincipals` — the app's people directory.
10
+ *
11
+ * There is NO access decision here, no session→token exchange, and no cookie is ever written.
12
+ *
13
+ * There is also NO local mode. The SDK verifies IAM-issued tokens and nothing else, everywhere. Local
14
+ * development runs the real stack (`bun run local:up`), where the proxy fronts each service and IAM
15
+ * authenticates through its own `LOCAL_DEV_LOGIN` bypass — one dev bypass in the system, owned by the
16
+ * service that owns identity.
17
+ */
18
+ import { type TokenAudienceProfile } from '@fabrika/auth-core';
19
+ import { IamClient } from './client';
20
+ import type { AuthContext, IamBinding, ListPrincipalsFailure, PrincipalList } from './types';
21
+ import { TokenVerifier } from './verify';
22
+ /** The env `createIam` reads. An app's Worker `Env` satisfies it structurally (extra fields are fine). */
23
+ export interface IamEnv {
24
+ /** The IAM service binding, or `HttpIamRpc` off Cloudflare. Typed by the contract — never the IAM Worker. */
25
+ IAM?: IamBinding;
26
+ /** IAM origin — the token `iss` this SDK verifies against. Canonicalized once in `createIam`. */
27
+ FABRIKA_IAM_ISSUER?: string;
28
+ /** The IAM app id `<app>@<env>` every deploy writes (`FABRIKA_IAM_APP_ID`); the fallback for `opts.appId`. */
29
+ FABRIKA_IAM_APP_ID?: string;
30
+ }
31
+ /** Options for `createIam`. `appId` falls back to `env.FABRIKA_IAM_APP_ID`. */
32
+ export interface CreateIamOptions {
33
+ /** The IAM app id: a platform service bakes its own in; an application reads `env.FABRIKA_IAM_APP_ID`. */
34
+ appId?: string;
35
+ /**
36
+ * Which tokens `authenticate` accepts. Defaults to the app's native tokens; a resource server
37
+ * passes `{ kind: 'resource', app, resource }` to accept OAuth tokens minted for that resource.
38
+ */
39
+ tokenAudience?: TokenAudienceProfile;
40
+ }
41
+ /** Why an authentication attempt produced no `AuthContext`. */
42
+ export type AuthFailureReason =
43
+ /** No proxy-injected token on the request — an anonymous endpoint, or nothing is in front of the app. */
44
+ 'no_token'
45
+ /** A token was presented and did not verify: signature, `iss`, `aud`, `exp` or claim shape. */
46
+ | 'invalid_token'
47
+ /** The credential resolved to nobody — IAM says the principal is gone or disabled. */
48
+ | 'unknown_principal'
49
+ /** IAM could not be consulted at all. NOT a decided negative — an incident, and never a 401. */
50
+ | 'unavailable';
51
+ /**
52
+ * The outcome of `authenticate`. A DECIDED NEGATIVE (`invalid_token`, `unknown_principal`) is not the
53
+ * same as "could not consult IAM" (`unavailable`, 503) — the same three-state rule `@fabrika/proxy`
54
+ * implements. `authenticate` throws for neither case.
55
+ */
56
+ export type AuthResult = {
57
+ ok: true;
58
+ context: AuthContext;
59
+ } | {
60
+ ok: false;
61
+ reason: AuthFailureReason;
62
+ status: 401 | 403 | 503;
63
+ };
64
+ /** Failure half of `AuthResult` — what a caller maps to its own error envelope. */
65
+ export type AuthFailure = Extract<AuthResult, {
66
+ ok: false;
67
+ }>;
68
+ /**
69
+ * The ANONYMOUS context — no principal, NO permissions (`can` → false, `scopedTo` → [] = none),
70
+ * `audit` a no-op. What an app sets on a request the proxy admitted through an anonymous endpoint: there is
71
+ * no token to verify, and nothing about the caller is known. An RPC procedure's `.require` on such a
72
+ * request would (correctly) 403.
73
+ */
74
+ export declare function anonymousContext(): AuthContext;
75
+ /** Internal construction config — `createIam` assembles this; apps never build it directly. */
76
+ interface IamConfig {
77
+ management: IamClient;
78
+ binding: IamBinding;
79
+ verifier: TokenVerifier;
80
+ appId: string;
81
+ tokenAudience: TokenAudienceProfile;
82
+ }
83
+ /**
84
+ * The request-time IAM surface. Build it with `createIam` (the only intended entry point — the
85
+ * constructor takes an internal config). Bundles token verification and `listPrincipals`, both bound
86
+ * to the env + app id resolved at construction.
87
+ */
88
+ export declare class Iam {
89
+ private readonly management;
90
+ private readonly binding;
91
+ private readonly verifier;
92
+ private readonly appId;
93
+ private readonly tokenAudience;
94
+ /** @internal — use `createIam`. */
95
+ constructor(config: IamConfig);
96
+ listPrincipals(req: Request): Promise<PrincipalList | ListPrincipalsFailure>;
97
+ /**
98
+ * Resolve the caller from the token the PROXY injected, verifying it locally against the JWKS.
99
+ * Never throws: an absent token, a token that does not verify, and an unreachable IAM are three
100
+ * distinct `ok: false` results.
101
+ */
102
+ authenticate(request: Request): Promise<AuthResult>;
103
+ /** Verify one token and wrap its claims in the `permits`-backed `AuthContext`. */
104
+ private contextFor;
105
+ }
106
+ /**
107
+ * The single request-time entry point. Reads `env.IAM` and the IAM environment names, and returns an
108
+ * `Iam` bound to them. Throws if the app id, the binding or the issuer is missing or unusable —
109
+ * there is no mode in which any of the three is optional.
110
+ */
111
+ export declare function createIam(env: IamEnv, opts?: CreateIamOptions): Iam;
112
+ export {};
@@ -0,0 +1,20 @@
1
+ export { IamClient } from './client';
2
+ export { applyScope } from './scope';
3
+ export { ForbiddenError, requirePermission, UnauthenticatedError } from './errors';
4
+ export type { HttpError } from './errors';
5
+ export { anonymousContext, createIam, Iam } from './iam';
6
+ export type { AuthFailure, AuthFailureReason, AuthResult, CreateIamOptions, IamEnv } from './iam';
7
+ export type { AuthCarrier } from './types';
8
+ export { DecodedIamMembership, HttpIamMembership, HttpIamRpc, IamTransportError } from './rpc-http';
9
+ export type { IamMembershipMethod } from './rpc-http';
10
+ export { createIamMembership, IamMembership } from './membership';
11
+ export type { IamMembershipEnv, MembershipRequestCaller } from './membership';
12
+ export type { FetchLike, HttpIamRpcOptions } from './rpc-http';
13
+ export { TokenVerifier } from './verify';
14
+ export type { VerifyResult } from './verify';
15
+ export { reconcileSchema, ReconcileSchemaError } from './provision';
16
+ export { appSchemaSchema } from './app-schema';
17
+ export type { ReconcileSchemaOptions } from './provision';
18
+ export type { AuthContext, IamBinding, ListPrincipalsFailure, PrincipalIdentity, PrincipalList } from './types';
19
+ export { PROXY_TOKEN_HEADER } from '@fabrika/auth-core';
20
+ export type { AppActionDef, AppSchema, AppScopeDef, DomainEvent, IamMembershipRpc, IamRpc, LinkTokenClaims, MembershipFailure, OAuthTokenClaims, PrincipalListItem, RoleDef, Scope, TokenAudienceProfile, } from '@fabrika/auth-core';
@@ -0,0 +1,46 @@
1
+ import type { AssignMemberResult, CreateScopeResult, FindMemberResult, IamMembershipRpc, InvitationOptions, IssueLinkResult, ListAppMembersResult, ListMembersResult, MemberTarget, RequestDelegationResult, RevokeLinkResult, RevokeMemberInput, RevokeMemberResult, Scope } from '@fabrika/auth-core';
2
+ /**
3
+ * The caller a membership request is made for: the verified token the proxy injected on THIS request, and
4
+ * the request's correlation id. IAM resolves the caller from the token and checks its live permissions; a
5
+ * delegation grant is bound to the request id, so both calls of one request carry the same one.
6
+ */
7
+ export interface MembershipRequestCaller {
8
+ readonly credential: string;
9
+ readonly requestId: string;
10
+ }
11
+ /** The env `createIamMembership` reads; an application's `env` satisfies it structurally. */
12
+ export interface IamMembershipEnv {
13
+ /** IAM's application entrypoint on Cloudflare, or `HttpIamRpc` off it. */
14
+ IAM?: IamMembershipRpc;
15
+ FABRIKA_IAM_APP_ID?: string;
16
+ }
17
+ /**
18
+ * The SDK half of the membership operations. It REQUESTS, on the caller's behalf: IAM decides, generates
19
+ * every value, writes every grant and issues every key. Nothing here mints, signs or names an actor, and
20
+ * every result is IAM's answer as it came.
21
+ */
22
+ export declare class IamMembership {
23
+ private readonly binding;
24
+ private readonly appId;
25
+ constructor(binding: IamMembershipRpc, appId: string);
26
+ /** A single-use grant for one request to `endpoint`, which the app's schema lists as a membership endpoint. */
27
+ requestDelegation(caller: MembershipRequestCaller, endpoint: string): Promise<RequestDelegationResult>;
28
+ /** A new value of `dimension`, with the caller granted the dimension's creator role in it. */
29
+ createScope(caller: MembershipRequestCaller, delegation: string, dimension: string): Promise<CreateScopeResult>;
30
+ /** `invitation` goes with an address only: its label, message and resend (`InvitationOptions`). */
31
+ assignMember(caller: MembershipRequestCaller, delegation: string, scope: Scope, role: string, member: MemberTarget, invitation?: InvitationOptions): Promise<AssignMemberResult>;
32
+ revokeMember(caller: MembershipRequestCaller, delegation: string, scope: Scope, member: RevokeMemberInput['member']): Promise<RevokeMemberResult>;
33
+ listMembers(caller: MembershipRequestCaller, scope: Scope): Promise<ListMembersResult>;
34
+ /** One page of the application's own people; `after` is the previous page's `next`. */
35
+ listAppMembers(caller: MembershipRequestCaller, after?: string): Promise<ListAppMembersResult>;
36
+ /** One principal among the members of `scope`, for an endpoint of `action` on, in or below its dimension. */
37
+ findMember(caller: MembershipRequestCaller, scope: Scope, principalId: string, action: string): Promise<FindMemberResult>;
38
+ /** A share link of `mount` for `chain[0]`; `chain` is the object and every value containing it, outward. */
39
+ issueLink(caller: MembershipRequestCaller, mount: string, chain: Scope[], days: number): Promise<IssueLinkResult>;
40
+ revokeLink(caller: MembershipRequestCaller, id: string): Promise<RevokeLinkResult>;
41
+ private caller;
42
+ }
43
+ /** Build the membership client from the env. Throws when the app id or the binding is missing. */
44
+ export declare function createIamMembership(env: IamMembershipEnv, opts?: {
45
+ appId?: string;
46
+ }): IamMembership;
@@ -0,0 +1,37 @@
1
+ import type { AppSchema } from '@fabrika/auth-core';
2
+ export interface ReconcileSchemaOptions {
3
+ /** The IAM Worker's admin origin, e.g. `https://iam.example.com` (trailing slash ok). */
4
+ url: string;
5
+ /** The app id; its FIRST reconcile registers it in IAM's app registry. */
6
+ app: string;
7
+ /** The app's declared vocabulary. */
8
+ schema: AppSchema;
9
+ /**
10
+ * Every origin IAM may hand this app a session at (ADR-0022), as known by the CONTROL PLANE.
11
+ *
12
+ * Omit to leave the registry alone; a supplied set REPLACES it, so it must be the complete set for
13
+ * the app and not just the environment being deployed. It is NEVER read off the `AppSchema`: an app
14
+ * that could name its own return origin could name any origin, which is the thing the registry
15
+ * exists to prevent. An empty array is a caller error: an app is never un-registered by having
16
+ * nothing to send.
17
+ */
18
+ returnOrigins?: readonly string[];
19
+ /**
20
+ * An IAM-issued `px_` ADMIN key, sent as `Authorization: Bearer`. Effectively required: IAM's
21
+ * `/admin/*` accepts only a bearer and refuses a credential-less call in every environment.
22
+ */
23
+ adminKey?: string;
24
+ /** Cancels the in-flight request when schema reconciliation belongs to a deploy lifecycle. */
25
+ signal?: AbortSignal;
26
+ }
27
+ /** Thrown when the admin endpoint rejects the reconcile; `status` is the HTTP status. */
28
+ export declare class ReconcileSchemaError extends Error {
29
+ readonly status: number;
30
+ constructor(message: string, status: number);
31
+ }
32
+ /**
33
+ * Reconcile one app's declared `AppSchema` into IAM (idempotent), then — when `returnOrigins` is
34
+ * supplied — project that set into IAM's return-origin registry. Resolves on success; throws
35
+ * `ReconcileSchemaError` on a non-2xx response, or `Error` on a half-set service token.
36
+ */
37
+ export declare function reconcileSchema(options: ReconcileSchemaOptions): Promise<void>;
@@ -0,0 +1,10 @@
1
+ export interface ForwardedCredentials {
2
+ /** The native credential (a `px_` key / `px_token`), or null when absent. */
3
+ credential: string | null;
4
+ /** cf-ray, or a fresh UUID when absent (e.g. local dev). */
5
+ requestId: string;
6
+ }
7
+ /** The correlation id for this request: cf-ray when the platform supplies one, else a fresh UUID. */
8
+ export declare function readRequestId(req: Request): string;
9
+ /** Pull the native credential and a correlation id off the request. `credential` may be null. */
10
+ export declare function readCredentials(req: Request): ForwardedCredentials;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * `IamRpc` over HTTP — the transport an app needs when it has no Cloudflare service binding.
3
+ *
4
+ * WHAT THIS IS NOT: it is not a second IAM. `IamRpc` (@fabrika/auth-core) is unchanged and this class
5
+ * holds no policy — it POSTs the method's input as JSON, reads the result back, and hands it to the
6
+ * same callers (`Iam`, `IamClient`) the binding serves. Everything downstream of the
7
+ * contract — local token verification, `can()`/`scopedTo()`, delegation checks inside the IAM
8
+ * service — is byte-for-byte the same code on both transports. The ONLY difference is how the
9
+ * call travels.
10
+ *
11
+ * THE WIRE IS `@fabrika/iam`'s `src/rpc-http.ts`, and that file is the contract:
12
+ *
13
+ * POST {origin}/rpc/<method> Authorization: Bearer <key> body = the method's input
14
+ * → 200 with the method's result (INCLUDING a decided `{ ok: false, reason }`)
15
+ * → 204 for `audit` (it returns void over the binding, so there is nothing to encode)
16
+ * → 401 bad/absent key · 404 no such method OR the surface is disabled (an unset key 404s) ·
17
+ * 400 undecodable body · 405 not a POST
18
+ *
19
+ * A DECIDED NEGATIVE IS A 200, and that distinction is the whole point: `{ ok: false, reason }` means
20
+ * "IAM answered: no", while a non-200 means "IAM could not be consulted". Only the second is an
21
+ * incident, and only the second throws here.
22
+ *
23
+ * ── Why a transport failure THROWS ────────────────────────────────────────────────────────────────
24
+ *
25
+ * Over a service binding an unreachable IAM is not a case a caller has to model: the binding either
26
+ * resolves or the whole invocation fails. Over HTTP it is a real state, and mapping it onto the
27
+ * contract's `reason` union would be a lie — `invalid_session` says "we checked, and no", which is not
28
+ * what happened. So it raises `IamTransportError`, callers see a 5xx, and nothing is ever admitted on
29
+ * the strength of an answer that never arrived. This is the same split `@fabrika/proxy`'s
30
+ * `HttpIamGateway` makes with `IamUnavailableError`.
31
+ *
32
+ * ── One instance per process, deliberately ────────────────────────────────────────────────────────
33
+ *
34
+ * The SDK caches the fetched JWKS and the tokens minted from `px_` share credentials in `WeakMap`s
35
+ * keyed by THE BINDING OBJECT. A fresh `HttpIamRpc` per request would therefore miss both caches every
36
+ * time and re-fetch the JWKS on every request. Build one for the process's lifetime and put it in the
37
+ * env, the way a binding lives for the isolate's lifetime.
38
+ *
39
+ * The key is a TRANSPORT credential, never a caller identity: it says "this request came from inside
40
+ * the deployment", exactly as a binding's unreachability does. Per-caller authorization still happens
41
+ * inside IAM, from the credential carried in the method's own input. It is never logged.
42
+ */
43
+ import type { ApproveDeviceAuthorizationInput, ApproveDeviceAuthorizationResult, AssignMemberInput, AssignMemberResult, AuditInput, CreateScopeInput, CreateScopeResult, DenyDeviceAuthorizationResult, FindMemberInput, FindMemberResult, IamMembershipRpc, IamRpc, InspectDeviceAuthorizationInput, InspectDeviceAuthorizationResult, IssueKeyInput, IssueKeyResult, IssueLinkInput, IssueLinkResult, Jwks, ListAppMembersInput, ListAppMembersResult, ListMembersInput, ListMembersResult, ListOwnDeviceAuthorizationsInput, ListOwnDeviceAuthorizationsResult, ListPrincipalsInput, ListPrincipalsResult, MintFromKeyInput, MintFromKeyResult, MintTokenInput, MintTokenResult, RequestDelegationInput, RequestDelegationResult, RevokeKeyInput, RevokeKeyResult, RevokeLinkInput, RevokeLinkResult, RevokeMemberInput, RevokeMemberResult } from '@fabrika/auth-core';
44
+ /**
45
+ * Just the call shape this client uses. Narrower than `typeof fetch` on purpose: the global carries
46
+ * runtime-specific extras (Bun's `preconnect`) that a test double has no business implementing.
47
+ */
48
+ export type FetchLike = (input: string, init: RequestInit) => Promise<Response>;
49
+ export interface HttpIamRpcOptions {
50
+ /** The IAM service origin, e.g. `http://iam:3000` on a private network (trailing slash tolerated). */
51
+ origin: string;
52
+ /**
53
+ * The bearer: the installation's shared secret gating `/rpc/*` for `HttpIamRpc`, or an application's own
54
+ * membership transport key for `HttpIamMembership` (`FABRIKA_IAM_RPC_KEY` in each). NEVER logged.
55
+ */
56
+ key: string;
57
+ /** Per-call timeout in milliseconds. */
58
+ timeoutMs?: number;
59
+ /** Injectable for tests; defaults to the global `fetch`. */
60
+ fetch?: FetchLike;
61
+ }
62
+ /**
63
+ * IAM could not be consulted — network failure, timeout, a non-2xx status, or a body that does not
64
+ * match the contract. Distinct from a decided denial, which is a normal result.
65
+ *
66
+ * Carries NO cause and no response body: a `fetch` error stringifies the request URL, and a body can
67
+ * quote a credential that was on the way in.
68
+ */
69
+ export declare class IamTransportError extends Error {
70
+ readonly method: string;
71
+ constructor(method: string);
72
+ }
73
+ export declare class HttpIamRpc implements IamRpc, IamMembershipRpc {
74
+ #private;
75
+ inspectDeviceAuthorization(input: InspectDeviceAuthorizationInput): Promise<InspectDeviceAuthorizationResult>;
76
+ approveDeviceAuthorization(input: ApproveDeviceAuthorizationInput): Promise<ApproveDeviceAuthorizationResult>;
77
+ denyDeviceAuthorization(input: InspectDeviceAuthorizationInput): Promise<DenyDeviceAuthorizationResult>;
78
+ listOwnDeviceAuthorizations(input: ListOwnDeviceAuthorizationsInput): Promise<ListOwnDeviceAuthorizationsResult>;
79
+ private readonly endpoint;
80
+ constructor(options: HttpIamRpcOptions);
81
+ mintToken(input: MintTokenInput): Promise<MintTokenResult>;
82
+ mintFromKey(input: MintFromKeyInput): Promise<MintFromKeyResult>;
83
+ issueKey(input: IssueKeyInput): Promise<IssueKeyResult>;
84
+ revokeKey(input: RevokeKeyInput): Promise<RevokeKeyResult>;
85
+ listPrincipals(input: ListPrincipalsInput): Promise<ListPrincipalsResult>;
86
+ requestDelegation(input: RequestDelegationInput): Promise<RequestDelegationResult>;
87
+ createScope(input: CreateScopeInput): Promise<CreateScopeResult>;
88
+ assignMember(input: AssignMemberInput): Promise<AssignMemberResult>;
89
+ revokeMember(input: RevokeMemberInput): Promise<RevokeMemberResult>;
90
+ listMembers(input: ListMembersInput): Promise<ListMembersResult>;
91
+ listAppMembers(input: ListAppMembersInput): Promise<ListAppMembersResult>;
92
+ findMember(input: FindMemberInput): Promise<FindMemberResult>;
93
+ issueLink(input: IssueLinkInput): Promise<IssueLinkResult>;
94
+ revokeLink(input: RevokeLinkInput): Promise<RevokeLinkResult>;
95
+ getJwks(): Promise<Jwks>;
96
+ audit(event: AuditInput): Promise<void>;
97
+ private call;
98
+ private send;
99
+ }
100
+ /** The membership operations, by name, as a transport carries them. */
101
+ export type IamMembershipMethod = keyof IamMembershipRpc;
102
+ /**
103
+ * `IamMembershipRpc` over any transport that answers each call with a JSON-shaped body: `HttpIamRpc`, or a Workers
104
+ * binding to IAM's application entrypoint. Every answer is decoded here, so a body that breaks the contract is
105
+ * `IamTransportError`, never a value of the declared type.
106
+ */
107
+ export declare class DecodedIamMembership implements IamMembershipRpc {
108
+ #private;
109
+ constructor(call: (method: IamMembershipMethod, input: unknown) => Promise<unknown>);
110
+ requestDelegation(input: RequestDelegationInput): Promise<RequestDelegationResult>;
111
+ createScope(input: CreateScopeInput): Promise<CreateScopeResult>;
112
+ assignMember(input: AssignMemberInput): Promise<AssignMemberResult>;
113
+ revokeMember(input: RevokeMemberInput): Promise<RevokeMemberResult>;
114
+ listMembers(input: ListMembersInput): Promise<ListMembersResult>;
115
+ listAppMembers(input: ListAppMembersInput): Promise<ListAppMembersResult>;
116
+ findMember(input: FindMemberInput): Promise<FindMemberResult>;
117
+ issueLink(input: IssueLinkInput): Promise<IssueLinkResult>;
118
+ revokeLink(input: RevokeLinkInput): Promise<RevokeLinkResult>;
119
+ }
120
+ /**
121
+ * `IamMembershipRpc` over IAM's membership transport, `POST {origin}/membership/<method>` (`@fabrika/iam`'s
122
+ * `src/membership-http.ts`): what an application without an IAM binding calls, with its own membership transport key.
123
+ * IAM serves no `requestDelegation` there, because only the proxy obtains a grant; calling it is `IamTransportError`.
124
+ */
125
+ export declare class HttpIamMembership extends DecodedIamMembership {
126
+ constructor(options: HttpIamRpcOptions);
127
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Resolve the three-state result from `scopedTo(action, dimension)` into a single value the
3
+ * app controls. This is the SINGLE place the empty-`IN ()` SQL trap is handled: `none`
4
+ * short-circuits without ever emitting `WHERE col IN ()`.
5
+ *
6
+ * - `null` → `all()` — unrestricted (admin / global grant): no filter
7
+ * - `[]` → `none()` — no access: empty result, DO NOT query
8
+ * - non-empty → `some(values)` — filter to these scope values (`WHERE col IN (...)`)
9
+ *
10
+ * The `values` are the opaque, app-owned scope values for that dimension (the app keys its
11
+ * rows by them). The app supplies what all/some/none mean for its own query; filtering must
12
+ * happen at the data layer, never by loading everything and filtering in memory.
13
+ */
14
+ export declare function applyScope<T>(scope: string[] | null, branches: {
15
+ all: () => T;
16
+ some: (values: string[]) => T;
17
+ none: () => T;
18
+ }): T;
@@ -0,0 +1,78 @@
1
+ import type { DomainEvent, IamRpc, LinkTokenClaims, OAuthTokenClaims, PrincipalListItem, PrincipalType, Scope } from '@fabrika/auth-core';
2
+ /**
3
+ * The `IamRpc` members the SDK calls. A Cloudflare application's `IAM` binding is IAM's
4
+ * `PropustkaApplication` entrypoint, which serves these and has no `issueKey` or `revokeKey`.
5
+ */
6
+ export type IamBinding = Pick<IamRpc, 'getJwks' | 'listPrincipals' | 'audit'>;
7
+ /**
8
+ * The resolved caller's identity — who Access says you are, as the IAM Worker recorded
9
+ * the principal. Exposed on `AuthContext` so apps can stamp domain rows (created_by,
10
+ * activity actor, assignee) and render "signed in as …" without a second lookup. It is
11
+ * NOT a permission surface — authorization is still `can()` / `scopedTo()`.
12
+ * - `id` — the IAM principal id (UUIDv7); stable, safe to store on domain rows.
13
+ * - `type` — 'user' (a human identity) or 'service' (a machine principal / API key).
14
+ * - `label` — human-readable: the user's email, or the service principal's name.
15
+ */
16
+ export interface PrincipalIdentity {
17
+ readonly id: string;
18
+ readonly type: PrincipalType;
19
+ readonly label: string;
20
+ }
21
+ /**
22
+ * The authenticated, ok:true result. `can`/`scopedTo` are local & pure (no binding call);
23
+ * only `audit` reaches the IAM Worker.
24
+ */
25
+ export interface AuthContext {
26
+ readonly ok: true;
27
+ /**
28
+ * The resolved caller identity (id / type / label), or `null` for an ANONYMOUS credential — a
29
+ * standalone (no-principal) share link. For stamping domain rows and
30
+ * display — NOT a permission surface (authorization stays `can` / `scopedTo`, which work the same
31
+ * either way). Stamp `created_by` with `principal?.id ?? null`.
32
+ */
33
+ readonly principal: PrincipalIdentity | null;
34
+ /**
35
+ * The OAuth client, consented scopes and grant behind an OAuth resource token; absent for every
36
+ * native credential. `perms` are already narrowed by the scopes, so `can()` stays the authority.
37
+ */
38
+ readonly oauth?: OAuthTokenClaims;
39
+ /**
40
+ * The share link behind a link key's token — its mount and the containment chain it is bound to —
41
+ * absent for every other credential. `principal` is then null.
42
+ */
43
+ readonly link?: LinkTokenClaims;
44
+ /**
45
+ * Point check: may this principal do `action` (optionally within `scope`, an opaque
46
+ * `{ type, value }` coordinate)? Scope-less → satisfied by GLOBAL permissions only; a
47
+ * scoped grant never widens into a scope-less allow.
48
+ */
49
+ can(action: string, scope?: Scope): boolean;
50
+ /**
51
+ * Scoping: which scope values may this principal perform `action` on within `dimension`
52
+ * (a scope type, e.g. `'organization'`)? Scopes are flat & independent — a list is always
53
+ * relative to one dimension.
54
+ * - `null` → unrestricted (holds the action globally — e.g. an admin)
55
+ * - `[]` → no access within this dimension at all
56
+ * - non-empty → exactly those (opaque, app-owned) scope values
57
+ * Consume via `applyScope` so the empty-`IN ()` trap is handled once. `dimension` is
58
+ * REQUIRED — multiple scope types may coexist, so the caller must say which one.
59
+ */
60
+ scopedTo(action: string, dimension: string): string[] | null;
61
+ /** Emit a domain audit event; app/principal/requestId are auto-attached. Fire-and-forget. */
62
+ audit(event: DomainEvent): Promise<void>;
63
+ }
64
+ /** Successful `listPrincipals` — the app's people directory (user principals). */
65
+ export interface PrincipalList {
66
+ readonly ok: true;
67
+ principals: PrincipalListItem[];
68
+ }
69
+ /** `listPrincipals` failure. missing/invalid → 401; unknown_principal/disabled/not_allowed → 403. */
70
+ export interface ListPrincipalsFailure {
71
+ readonly ok: false;
72
+ reason: 'missing_token' | 'invalid_token' | 'unknown_principal' | 'disabled' | 'not_allowed';
73
+ status: 401 | 403;
74
+ }
75
+ /** A request context with a writable `auth` slot, set to the resolved `AuthContext` (or null for an anonymous caller). */
76
+ export interface AuthCarrier {
77
+ auth?: AuthContext | null;
78
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Local access-token verification — the only thing this SDK still does with a credential.
3
+ *
4
+ * Since [ADR-0022](../../../docs/decisions/0022-the-proxy-is-the-only-enforcement-point.md) the PROXY is the only
5
+ * enforcement point: it selects the endpoint, resolves the credential, and injects the verified
6
+ * token as `PROXY_TOKEN_HEADER`. The app must NOT trust that header blindly, so every token is
7
+ * re-verified here against IAM's published JWKS — signature, `iss`, `aud`, `exp` — before it can
8
+ * become an `AuthContext`.
9
+ *
10
+ * The verification itself is `@fabrika/jwks`, shared byte-for-byte with the proxy's enforcement path.
11
+ * What this file owns is the SDK's construction shape: the audience is fixed at construction, and the
12
+ * key set plus the unknown-`kid` cooldown are cached PER BINDING in a module-level WeakMap rather than
13
+ * per instance — a service may build one on every request, so an SDK object
14
+ * built there is per-request while the binding lives for the isolate/process.
15
+ */
16
+ import type { TokenAudienceProfile } from '@fabrika/auth-core';
17
+ import { type VerifyResult } from '@fabrika/jwks';
18
+ import type { IamBinding } from './types';
19
+ export type { VerifyResult };
20
+ /**
21
+ * Verifies tokens for ONE (binding, issuer, audience) triple. A bare string audience is an app id. Cheap to
22
+ * construct; the cache is shared.
23
+ */
24
+ export declare class TokenVerifier {
25
+ private readonly audience;
26
+ private readonly inner;
27
+ constructor(binding: IamBinding, issuer: string, audience: string | TokenAudienceProfile, now?: () => number);
28
+ verify(token: string, audience?: string | TokenAudienceProfile): Promise<VerifyResult>;
29
+ }