@fabrika/iam-contract 0.8.2 → 0.9.0-cloudflare.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/iam-contract",
3
- "version": "0.8.2",
3
+ "version": "0.9.0-cloudflare.2",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -11,12 +11,13 @@
11
11
  "access": "public"
12
12
  },
13
13
  "files": [
14
- "src"
14
+ "src",
15
+ "types"
15
16
  ],
16
17
  "exports": {
17
18
  ".": {
18
19
  "bun": "./src/index.ts",
19
- "types": "./src/index.ts",
20
+ "types": "./types/src/index.d.ts",
20
21
  "default": "./src/index.ts"
21
22
  }
22
23
  },
@@ -24,7 +25,8 @@
24
25
  "typecheck": "tsc --noEmit"
25
26
  },
26
27
  "dependencies": {
27
- "@fabrika/app": "0.8.2",
28
- "@fabrika/auth-core": "0.8.2"
28
+ "@fabrika/app": "0.9.0-cloudflare.2",
29
+ "@fabrika/auth-core": "0.9.0-cloudflare.2",
30
+ "@fabrika/proxy-core": "0.9.0-cloudflare.2"
29
31
  }
30
32
  }
package/src/access.ts CHANGED
@@ -11,6 +11,8 @@ export const IAM_ACTIONS = Object.freeze({
11
11
  ADMIN: 'iam.admin',
12
12
  /** Manage the people of applications, never of the installation's own apps. Global, or in `app` values. */
13
13
  USERS_MANAGE: 'iam.users.manage',
14
+ /** Enrol a browser as a terminal (ADR-0067). Global only; its profile and revocation stay `iam.admin`'s. */
15
+ TERMINALS_MANAGE: 'iam.terminals.manage',
14
16
  })
15
17
 
16
18
  export const IAM_SCOPES = Object.freeze({
@@ -40,6 +42,7 @@ export const IAM_APP_SCHEMA: AppSchema = {
40
42
  actions: [
41
43
  { action: IAM_ACTIONS.ADMIN, description: 'Administer identity and access for the installation' },
42
44
  { action: IAM_ACTIONS.USERS_MANAGE, description: 'Manage the people of applications: invite, sign out, reset, grant and revoke' },
45
+ { action: IAM_ACTIONS.TERMINALS_MANAGE, description: 'Enrol a shared computer as a terminal' },
43
46
  ],
44
47
  roles: {
45
48
  [USER_ADMIN_ROLE]: {
package/src/index.ts CHANGED
@@ -24,6 +24,8 @@ import type {
24
24
  RoleDef,
25
25
  WorkloadKind,
26
26
  } from '@fabrika/auth-core'
27
+ import type { InstallationBrand } from '@fabrika/proxy-core/brand'
28
+ import type { PasswordPolicyDto, SetPasswordPolicyRequest } from './password-policy'
27
29
 
28
30
  // ── Common wrappers ───────────────────────────────────────────────────────────
29
31
 
@@ -81,17 +83,40 @@ export interface GrantDto {
81
83
  export interface PrincipalDetail extends PrincipalListItem {
82
84
  /** The mailbox is listed in `IAM_BOOTSTRAP_ADMINS`: an internal user with it is an installation admin. */
83
85
  bootstrapMailbox: boolean
84
- grants: GrantDto[]
85
- /** Effective permissions with `source` (grant / bootstrap / group:<ref>). */
86
+ grants: PrincipalGrantDto[]
87
+ /** Permissions at IAM only. Prefer `access` for the per-application view. */
86
88
  permissions: PermissionEntry[]
89
+ access: PrincipalAppAccess[]
90
+ /** Presentation capabilities. Every mutation still checks the caller live. */
91
+ management: { account: boolean; passwordLinks: boolean; audit: boolean }
92
+ activatedAt: number | null
93
+ disabledAt: number | null
87
94
  authentication: {
88
95
  /** OIDC is linked when this user has claimed an identity-provider subject. */
89
96
  oidc: { state: 'unavailable' | 'unlinked' | 'linked' }
90
97
  /** Password access is globally opt-in and then enabled per user. */
91
- password: { state: 'unavailable' | 'disabled' | 'pending' | 'enabled' }
98
+ password: {
99
+ state: 'unavailable' | 'disabled' | 'pending' | 'enabled'
100
+ delivery: 'email' | 'manual'
101
+ latestAction: { purpose: 'enrollment' | 'reset'; createdAt: number; expiresAt: number; closedAt: number | null } | null
102
+ }
92
103
  }
93
104
  }
94
105
 
106
+ export interface PrincipalGrantDto extends GrantDto {
107
+ roleName: string | null
108
+ rolePermissions: string[]
109
+ grantedByLabel: string | null
110
+ canManage: boolean
111
+ }
112
+
113
+ export interface PrincipalAppAccess {
114
+ app: string
115
+ /** Resolved by IAM using the same per-app resolver as token issuance; empty for a disabled account. */
116
+ permissions: PermissionEntry[]
117
+ lastSignInAt: number | null
118
+ }
119
+
95
120
  export interface InviteRequest {
96
121
  email: string
97
122
  /** Omitted: `internal`. An `external` user signs in by password only, and OIDC never claims it. */
@@ -111,8 +136,11 @@ export interface UpdatePrincipalRequest {
111
136
  export interface SessionDto {
112
137
  id: string
113
138
  principalId: string
114
- /** How the human proved who they were; `local_dev` is IAM's `LOCAL_DEV_LOGIN` bypass. */
115
- authenticationMethod: 'oidc' | 'password' | 'local_dev'
139
+ /**
140
+ * How the human proved who they were; `local_dev` is IAM's `LOCAL_DEV_LOGIN` bypass, `terminal_assertion` an
141
+ * application's word about who stands at an enrolled terminal.
142
+ */
143
+ authenticationMethod: 'oidc' | 'password' | 'local_dev' | 'terminal_assertion'
116
144
  /** The app this session is bound to (an ADR-0022 child); null = the IAM session itself. */
117
145
  app: string | null
118
146
  /** The IAM session this was derived from; null on an IAM session. Revoking that one revokes this. */
@@ -122,6 +150,13 @@ export interface SessionDto {
122
150
  createdAt: number
123
151
  expiresAt: number
124
152
  revokedAt: number | null
153
+ /** Why this session cannot be used now, independently of its own stored lifecycle. */
154
+ unavailableReason: 'revoked' | 'expired' | 'parent_ended' | 'account_disabled' | 'method_disabled' | null
155
+ }
156
+
157
+ export interface SessionList extends CursorList<SessionDto> {
158
+ /** App sessions belonging to the sign-ins on this page, when `signInsOnly` is requested. */
159
+ applications: SessionDto[]
125
160
  }
126
161
 
127
162
  // ── Grants ────────────────────────────────────────────────────────────────────
@@ -141,6 +176,11 @@ export interface CreateGrantRequest {
141
176
  expiresAt?: number | null
142
177
  }
143
178
 
179
+ /** Change access within its original application, preserving the grant's identity and author. */
180
+ export interface UpdateGrantInput extends Omit<CreateGrantRequest, 'principalId' | 'app'> {
181
+ id: string
182
+ }
183
+
144
184
  // ── Apps (read-only; the live registry derived from the schema tables) ────────
145
185
 
146
186
  /** The set of app ids propustka knows about — the choices for a grant's `app`. */
@@ -155,11 +195,8 @@ export interface AppDto {
155
195
  // ── Roles & policies (per-app, DB-backed) ─────────────────────────────────────
156
196
 
157
197
  /** A role available for the calling app — a built-in or a DB row (app/custom). */
158
- export interface RoleDto {
198
+ export interface RoleDto extends RoleDef {
159
199
  key: string
160
- name: string
161
- description?: string
162
- permissions: string[]
163
200
  /** 'builtin' (cross-app, e.g. admin), 'app' (reconciled), or 'custom' (admin-made). */
164
201
  origin: 'builtin' | 'app' | 'custom'
165
202
  }
@@ -192,6 +229,8 @@ export interface AppSchemaDto {
192
229
  members?: { manage: string; list: string }
193
230
  /** `labels` when the app's directory shows labels to `members.list` holders. */
194
231
  directory?: 'labels'
232
+ /** The app's identification page at a terminal (ADR-0067); absent when the app declares none. */
233
+ terminal?: { identify: string }
195
234
  }
196
235
 
197
236
  // ── Policies (origin='custom' roles, admin-composed) ──────────────────────────
@@ -231,8 +270,9 @@ export interface ApiKeyDto {
231
270
  }
232
271
 
233
272
  /**
234
- * One API key with its live credential. Its permissions are its grants, edited with `grants.create` and
235
- * `grants.delete` by an installation administrator and read again at every token exchange.
273
+ * One API key with its live credential, as `apiKeys.get` and each row of `apiKeys.list` answer it. Its permissions are
274
+ * its grants, edited with `grants.create` and `grants.delete` by an installation administrator and read again at every
275
+ * token exchange. An IAM older than the list's credential answers list rows without `credential`.
236
276
  */
237
277
  export interface ApiKeyDetail extends ApiKeyDto {
238
278
  /** The key's live credential, or null when none is live (a revoked key). */
@@ -398,6 +438,7 @@ export interface PrincipalIdInput {
398
438
  export interface ListSessionsInput extends PageInput {
399
439
  /** Whose sessions to list. */
400
440
  principalId: string
441
+ signInsOnly?: boolean
401
442
  }
402
443
 
403
444
  export interface SessionIdInput {
@@ -471,7 +512,14 @@ export interface ListAuditInput {
471
512
  resourceType?: string
472
513
  resourceId?: string
473
514
  principalId?: string
515
+ /** The person affected by the change, distinct from `principalId` (the actor). */
516
+ subjectPrincipalId?: string
474
517
  action?: string
518
+ /**
519
+ * Leave out the admin reads (`iam.admin.access`), which IAM writes for every `/admin/*` request, so a feed of what
520
+ * changed reads changes and not the reads that page through it. An IAM older than this field ignores it.
521
+ */
522
+ changesOnly?: boolean
475
523
  requestId?: string
476
524
  before?: string
477
525
  limit?: number
@@ -557,6 +605,45 @@ export interface IssueMembershipKeyResponse {
557
605
  expiresAt: number
558
606
  }
559
607
 
608
+ // ── Terminals (ADR-0067) ──────────────────────────────────────────────────────
609
+
610
+ /** An application that may assert a person on a terminal, with the action patterns of the person that survive there. */
611
+ export interface TerminalAsserterDto {
612
+ app: string
613
+ ceiling: string[]
614
+ }
615
+
616
+ /**
617
+ * An enrolled browser, as an operator sees it. Its own grants are ordinary grants on `terminalId`, which is its service
618
+ * principal; the cookie is never shown.
619
+ */
620
+ export interface TerminalDto {
621
+ terminalId: string
622
+ label: string
623
+ /** The administrator who enrolled it; null once that principal is gone. */
624
+ enrolledBy: string | null
625
+ /** Seconds a person's session on it lasts. */
626
+ sessionTtl: number
627
+ asserters: TerminalAsserterDto[]
628
+ status: 'active' | 'revoked'
629
+ createdAt: number
630
+ lastSeenAt: number | null
631
+ revokedAt: number | null
632
+ }
633
+
634
+ export type ListTerminalsInput = PageInput
635
+
636
+ export interface TerminalIdInput {
637
+ terminalId: string
638
+ }
639
+
640
+ /** Replace a terminal's whole profile. The ceiling is empty by default: the person brings only their identity. */
641
+ export interface SetTerminalProfileRequest extends TerminalIdInput {
642
+ label: string
643
+ sessionTtl: number
644
+ asserters: TerminalAsserterDto[]
645
+ }
646
+
560
647
  // ── An application's own people, administered by its own administrator ───────
561
648
 
562
649
  /**
@@ -612,6 +699,21 @@ export interface RevokeAppRoleResponse {
612
699
  revoked: boolean
613
700
  }
614
701
 
702
+ /**
703
+ * The installation's client logo on fabrika's own pages (ADR-0066): IAM's sign-in, account and invitation pages, and the
704
+ * proxy's error pages on every application host. `null` shows the fabrika mark. An installation serves one client, so
705
+ * this is one setting of the installation, kept by IAM.
706
+ */
707
+ export interface InstallationBrandDto {
708
+ brand: InstallationBrand | null
709
+ /** When it was last set or cleared, in epoch seconds; null when it never was. */
710
+ updatedAt: number | null
711
+ }
712
+
713
+ export interface SetInstallationBrandRequest {
714
+ brand: InstallationBrand
715
+ }
716
+
615
717
  export interface IamAdminRpcContract {
616
718
  me: RpcProcedure<void, MeDto>
617
719
  principals: {
@@ -630,7 +732,7 @@ export interface IamAdminRpcContract {
630
732
  * derived from it — a child is only valid while its parent is — so this is one call, not a sweep.
631
733
  */
632
734
  sessions: {
633
- list: RpcProcedure<ListSessionsInput, CursorList<SessionDto>>
735
+ list: RpcProcedure<ListSessionsInput, SessionList>
634
736
  revoke: RpcProcedure<SessionIdInput, RevokedSessionsResponse>
635
737
  revokeAll: RpcProcedure<PrincipalIdInput, RevokedSessionsResponse>
636
738
  }
@@ -642,8 +744,13 @@ export interface IamAdminRpcContract {
642
744
  grants: {
643
745
  /** A role grant the principal already holds — same role, scope and app — answers 409 naming it. */
644
746
  create: RpcProcedure<CreateGrantRequest, GrantDto>
747
+ update: RpcProcedure<UpdateGrantInput, GrantDto>
645
748
  delete: RpcProcedure<GrantIdInput, OkResponse>
646
749
  }
750
+ connections: {
751
+ list: RpcProcedure<ListSessionsInput, CursorList<UserConnectionDto>>
752
+ revoke: RpcProcedure<GrantIdInput, OkResponse>
753
+ }
647
754
  apps: {
648
755
  list: RpcProcedure<void, ListResponse<AppDto>>
649
756
  getSchema: RpcProcedure<AppInput, AppSchemaDto>
@@ -659,7 +766,7 @@ export interface IamAdminRpcContract {
659
766
  delete: RpcProcedure<PolicyIdInput, OkResponse>
660
767
  }
661
768
  apiKeys: {
662
- list: RpcProcedure<ListApiKeysInput, CursorList<ApiKeyDto>>
769
+ list: RpcProcedure<ListApiKeysInput, CursorList<ApiKeyDetail>>
663
770
  get: RpcProcedure<ApiKeyIdInput, ApiKeyDetail>
664
771
  provision: RpcProcedure<ProvisionApiKeyRequest, ProvisionApiKeyResponse>
665
772
  rotate: RpcProcedure<RotateApiKeyInput, RotateApiKeyResponse>
@@ -681,6 +788,15 @@ export interface IamAdminRpcContract {
681
788
  list: RpcProcedure<ListAuditInput, CursorList<AuditEventDto>>
682
789
  listAuthLog: RpcProcedure<ListAuthLogInput, CursorList<AuthLogDto>>
683
790
  }
791
+ /**
792
+ * Enrolled terminals. A browser is enrolled at IAM's `/auth/terminals/enroll`, never here. Revoking a terminal ends
793
+ * every session issued on it.
794
+ */
795
+ terminals: {
796
+ list: RpcProcedure<ListTerminalsInput, CursorList<TerminalDto>>
797
+ setProfile: RpcProcedure<SetTerminalProfileRequest, TerminalDto>
798
+ revoke: RpcProcedure<TerminalIdInput, OkResponse>
799
+ }
684
800
  /**
685
801
  * An application's own people, administered by whoever holds the application's `members.manage` globally there: no
686
802
  * installation admin needed. Each change writes its audit row under the application.
@@ -691,8 +807,39 @@ export interface IamAdminRpcContract {
691
807
  grant: RpcProcedure<GrantAppRoleRequest, GrantAppRoleResponse>
692
808
  revoke: RpcProcedure<RevokeAppRoleRequest, RevokeAppRoleResponse>
693
809
  }
810
+ /**
811
+ * The installation's client logo (ADR-0066), an installation administrator's (`iam.admin`) to read and to change. A
812
+ * change writes `iam.brand.update` in its own transaction. IAM's pages show it on their next render; the proxy's
813
+ * pages once Control republishes their manifests.
814
+ */
815
+ brand: {
816
+ get: RpcProcedure<void, InstallationBrandDto>
817
+ set: RpcProcedure<SetInstallationBrandRequest, InstallationBrandDto>
818
+ clear: RpcProcedure<void, InstallationBrandDto>
819
+ }
820
+ /**
821
+ * The installation's minimum password length (ADR-0020), an installation administrator's (`iam.admin`) to read and to
822
+ * change. A change writes `iam.password_policy.update` in its own transaction and applies to the next password set,
823
+ * reset or chosen at an invitation; stored passwords stay as they are.
824
+ */
825
+ passwordPolicy: {
826
+ get: RpcProcedure<void, PasswordPolicyDto>
827
+ set: RpcProcedure<SetPasswordPolicyRequest, PasswordPolicyDto>
828
+ }
829
+ }
830
+
831
+ export interface UserConnectionDto {
832
+ id: string
833
+ clientId: string
834
+ app: string
835
+ resource: string
836
+ scopes: string[]
837
+ createdAt: number
838
+ expiresAt: number
839
+ status: 'active' | 'revoked' | 'expired' | 'unavailable'
694
840
  }
695
841
 
696
842
  export type { AppActionDef, AppMemberItem, AppSchema, AppScopeDef, PermissionEntry, PermissionSource, PrincipalAffiliation, PrincipalType, RoleDef }
697
843
  export * from './access'
698
844
  export * from './oauth'
845
+ export * from './password-policy'
package/src/oauth.ts CHANGED
@@ -8,15 +8,34 @@ export interface OAuthManagedClient {
8
8
  redirectUris: string[]
9
9
  }
10
10
 
11
- export interface OAuthClientMetadataDocument {
11
+ /** RFC 7523 §2.2: the `client_assertion_type` of a JWT client assertion (`private_key_jwt`). */
12
+ export const OAUTH_CLIENT_ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
13
+
14
+ /** The signing algorithms IAM verifies a client assertion with. */
15
+ export const OAUTH_CLIENT_ASSERTION_ALGORITHMS = ['RS256'] as const
16
+ export type OAuthClientAssertionAlgorithm = (typeof OAUTH_CLIENT_ASSERTION_ALGORITHMS)[number]
17
+
18
+ interface OAuthClientMetadataFields {
12
19
  client_id: string
13
20
  client_name?: string
14
21
  redirect_uris: string[]
15
- token_endpoint_auth_method: 'none'
16
22
  grant_types: ('authorization_code' | 'refresh_token')[]
17
23
  response_types: ['code']
18
24
  }
19
25
 
26
+ /** A public client, or one that authenticates with a JWT signed by a key its document publishes. */
27
+ export type OAuthClientMetadataDocument =
28
+ | (OAuthClientMetadataFields & { token_endpoint_auth_method: 'none' })
29
+ | (OAuthClientMetadataFields & {
30
+ token_endpoint_auth_method: 'private_key_jwt'
31
+ /** HTTPS, on the client id's own origin. */
32
+ jwks_uri: string
33
+ token_endpoint_auth_signing_alg: OAuthClientAssertionAlgorithm
34
+ })
35
+
36
+ /** How a token or revocation request authenticates its client: no credential, or a JWT assertion. */
37
+ export type OAuthClientCredential = { method: 'none' } | { method: 'private_key_jwt'; assertion: string }
38
+
20
39
  export interface OAuthResourceDto {
21
40
  app: string
22
41
  resource: string
@@ -84,7 +103,12 @@ export interface OAuthAuthorizationServerMetadata {
84
103
  response_types_supported: ['code']
85
104
  grant_types_supported: ['authorization_code', 'refresh_token']
86
105
  code_challenge_methods_supported: ['S256']
87
- token_endpoint_auth_methods_supported: ['none']
106
+ token_endpoint_auth_methods_supported: ['none', 'private_key_jwt']
107
+ token_endpoint_auth_signing_alg_values_supported: OAuthClientAssertionAlgorithm[]
108
+ revocation_endpoint_auth_methods_supported: ['none', 'private_key_jwt']
109
+ revocation_endpoint_auth_signing_alg_values_supported: OAuthClientAssertionAlgorithm[]
110
+ /** RFC 9207: every authorization response carries `iss`. */
111
+ authorization_response_iss_parameter_supported: true
88
112
  client_id_metadata_document_supported: true
89
113
  }
90
114
 
@@ -126,27 +150,50 @@ export function parseOAuthClientMetadata(value: unknown, registration: OAuthMana
126
150
  const responses = rawResponses === undefined ? ['code'] : rawResponses
127
151
  if (
128
152
  managed === null || clientId !== managed.clientId || uris === null || !uris.every((uri) => managed.redirectUris.includes(uri))
129
- || field(value, 'token_endpoint_auth_method') !== 'none' || (clientName !== undefined && !isNonemptyText(clientName))
153
+ || (clientName !== undefined && !isNonemptyText(clientName))
130
154
  || field(value, 'client_secret') !== undefined || field(value, 'client_secret_expires_at') !== undefined || grants === null
131
155
  || !Array.isArray(responses) || responses.length !== 1 || responses[0] !== 'code'
132
156
  ) return null
133
- const metadata: OAuthClientMetadataDocument = {
157
+ const fields: OAuthClientMetadataFields = {
134
158
  client_id: managed.clientId,
135
159
  redirect_uris: uris,
136
- token_endpoint_auth_method: 'none',
137
160
  grant_types: grants,
138
161
  response_types: ['code'],
139
162
  }
140
- if (isNonemptyText(clientName)) metadata.client_name = clientName
141
- return metadata
163
+ if (isNonemptyText(clientName)) fields.client_name = clientName
164
+ const method = field(value, 'token_endpoint_auth_method')
165
+ if (method === 'none') return { ...fields, token_endpoint_auth_method: 'none' }
166
+ if (method !== 'private_key_jwt') return null
167
+ const jwksUri = field(value, 'jwks_uri')
168
+ const algorithm = field(value, 'token_endpoint_auth_signing_alg')
169
+ // An inline `jwks` would bypass the origin rule below; RFC 7591 §2 forbids it beside `jwks_uri` anyway.
170
+ if (field(value, 'jwks') !== undefined || !isClientOriginUrl(jwksUri, managed.clientId) || !isClientAssertionAlgorithm(algorithm)) return null
171
+ return { ...fields, token_endpoint_auth_method: 'private_key_jwt', jwks_uri: jwksUri, token_endpoint_auth_signing_alg: algorithm }
172
+ }
173
+
174
+ function isClientAssertionAlgorithm(value: unknown): value is OAuthClientAssertionAlgorithm {
175
+ return OAUTH_CLIENT_ASSERTION_ALGORITHMS.some((algorithm) => algorithm === value)
176
+ }
177
+
178
+ /** An HTTPS URL on the client id's own origin, so the client's own host vouches for its keys. */
179
+ function isClientOriginUrl(value: unknown, clientId: string): value is string {
180
+ if (typeof value !== 'string' || value.includes('#')) return false
181
+ try {
182
+ const url = new URL(value)
183
+ return url.protocol === 'https:' && url.username === '' && url.password === '' && url.origin === new URL(clientId).origin
184
+ } catch {
185
+ return false
186
+ }
142
187
  }
143
188
 
144
189
  function parseClientGrantTypes(value: unknown): OAuthClientMetadataDocument['grant_types'] | null {
145
190
  if (value === undefined) return ['authorization_code']
146
- if (!Array.isArray(value) || value.length === 0 || value.length > 2) return null
191
+ if (!Array.isArray(value) || value.length === 0 || value.length > 16) return null
147
192
  const grants: OAuthClientMetadataDocument['grant_types'] = []
148
193
  for (const grant of value) {
149
- if ((grant !== 'authorization_code' && grant !== 'refresh_token') || grants.includes(grant)) return null
194
+ if (typeof grant !== 'string') return null
195
+ // A client may list grants IAM does not serve (Claude lists jwt-bearer); IAM serves the intersection.
196
+ if ((grant !== 'authorization_code' && grant !== 'refresh_token') || grants.includes(grant)) continue
150
197
  grants.push(grant)
151
198
  }
152
199
  return grants.includes('authorization_code') ? grants : null
@@ -261,6 +308,20 @@ export function parseOAuthTokenRequest(value: unknown): OAuthTokenRequest | null
261
308
  return null
262
309
  }
263
310
 
311
+ const MAX_CLIENT_ASSERTION_LENGTH = 16_384
312
+
313
+ /**
314
+ * The client credential of a token or revocation request (RFC 7521 §4.2). Neither assertion field is no
315
+ * credential; a half-present pair, another assertion type or an oversized assertion is null.
316
+ */
317
+ export function parseOAuthClientCredential(value: unknown): OAuthClientCredential | null {
318
+ const type = field(value, 'client_assertion_type')
319
+ const assertion = field(value, 'client_assertion')
320
+ if (type === undefined && assertion === undefined) return { method: 'none' }
321
+ if (type !== OAUTH_CLIENT_ASSERTION_TYPE || !isNonemptyText(assertion) || assertion.length > MAX_CLIENT_ASSERTION_LENGTH) return null
322
+ return { method: 'private_key_jwt', assertion }
323
+ }
324
+
264
325
  export function parseOAuthRevocationRequest(value: unknown): OAuthRevocationRequest | null {
265
326
  const clientId = field(value, 'client_id')
266
327
  const token = field(value, 'token')
@@ -0,0 +1,43 @@
1
+ // The installation's password policy (ADR-0020): one minimum length for every principal of the installation, which an
2
+ // installation administrator may lower from the default down to the runtime's floor. IAM checks every value again; the
3
+ // console reads these numbers only to say what IAM will accept.
4
+
5
+ /** The minimum of an installation that never set one: NIST SP 800-63B's for a password that is the only factor. */
6
+ export const PASSWORD_DEFAULT_MIN_LENGTH = 15
7
+
8
+ /**
9
+ * The lowest minimum IAM accepts on a runtime whose verifiers meet OWASP's PBKDF2 cost. Below it a password is a PIN, and
10
+ * no throttle makes it one a person can be trusted with.
11
+ */
12
+ export const PASSWORD_MIN_LENGTH_FLOOR = 4
13
+
14
+ /** The most code points a password may have, whatever the minimum. */
15
+ export const PASSWORD_MAX_LENGTH = 256
16
+
17
+ /** Below this NIST asks for a second factor, which IAM does not have; the console says so. */
18
+ export const PASSWORD_SINGLE_FACTOR_FLOOR = 8
19
+
20
+ /** The password policy as IAM applies it now. */
21
+ export interface PasswordPolicyDto {
22
+ /** The minimum length in Unicode code points that IAM applies to every new password. */
23
+ minLength: number
24
+ /** The minimum of an installation that never set one, `PASSWORD_DEFAULT_MIN_LENGTH`. */
25
+ defaultMinLength: number
26
+ /**
27
+ * The lowest minimum this IAM accepts. `PASSWORD_MIN_LENGTH_FLOOR` where verifiers meet OWASP's PBKDF2 cost; the
28
+ * default on a runtime whose cost is lower (a Cloudflare Worker, backlog 201), where a short password is cheap to
29
+ * crack offline.
30
+ */
31
+ minLengthFloor: number
32
+ /** The most code points a password may have. */
33
+ maxLength: number
34
+ /** Whether the installation enables password sign-in at all; the minimum waits for it when it does not. */
35
+ passwordEnabled: boolean
36
+ /** When an administrator last changed the minimum, in epoch seconds; null when nobody ever did. */
37
+ updatedAt: number | null
38
+ }
39
+
40
+ export interface SetPasswordPolicyRequest {
41
+ /** A whole number from `minLengthFloor` to `maxLength`. */
42
+ minLength: number
43
+ }
@@ -0,0 +1,27 @@
1
+ import { type AppSchema, type PermissionEntry } from '@fabrika/auth-core';
2
+ /** Durable IAM application id of IAM itself — a deployed identity, not configuration branding (ADR-0024). */
3
+ export declare const IAM_APP_ID = "propustka";
4
+ export declare const IAM_ACTIONS: Readonly<{
5
+ /** Administer identity and access for the installation. Global only; it reaches every application. */
6
+ ADMIN: "iam.admin";
7
+ /** Manage the people of applications, never of the installation's own apps. Global, or in `app` values. */
8
+ USERS_MANAGE: "iam.users.manage";
9
+ /** Enrol a browser as a terminal (ADR-0067). Global only; its profile and revocation stay `iam.admin`'s. */
10
+ TERMINALS_MANAGE: "iam.terminals.manage";
11
+ }>;
12
+ export declare const IAM_SCOPES: Readonly<{
13
+ /** One application environment, by its IAM id `<app>@<env>`. */
14
+ APP: "app";
15
+ }>;
16
+ /** The preset role a user administrator holds. */
17
+ export declare const USER_ADMIN_ROLE = "user-admin";
18
+ /** What a holder of IAM permissions administers: the installation, or the people of some applications (`null` = all). */
19
+ export type IamAdministrationScope = {
20
+ readonly kind: 'installation';
21
+ } | {
22
+ readonly kind: 'users';
23
+ readonly apps: readonly string[] | null;
24
+ };
25
+ /** The administration IAM permissions confer, or null for none. IAM adds that a user administrator acts from a session. */
26
+ export declare function iamAdministrationScope(permissions: PermissionEntry[]): IamAdministrationScope | null;
27
+ export declare const IAM_APP_SCHEMA: AppSchema;