@owlmeans/iam 0.1.18-rc.8 → 0.1.18

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.
Files changed (110) hide show
  1. package/README.md +185 -15
  2. package/agent-meta/manifest.json +92 -4
  3. package/agent-meta/skills/iam/SKILL.md +91 -177
  4. package/agent-meta/skills/iam/parts/clients.md +93 -0
  5. package/agent-meta/skills/iam/parts/permissions.md +106 -0
  6. package/agent-meta/skills/iam/parts/service.md +115 -0
  7. package/agent-meta/skills/iam/parts/tenancy.md +72 -0
  8. package/build/consts.d.ts +170 -1
  9. package/build/consts.d.ts.map +1 -1
  10. package/build/consts.js +221 -0
  11. package/build/consts.js.map +1 -1
  12. package/build/consts.local.d.ts +58 -0
  13. package/build/consts.local.d.ts.map +1 -0
  14. package/build/consts.local.js +21 -0
  15. package/build/consts.local.js.map +1 -0
  16. package/build/errors.d.ts +26 -0
  17. package/build/errors.d.ts.map +1 -1
  18. package/build/errors.js +38 -0
  19. package/build/errors.js.map +1 -1
  20. package/build/facet.d.ts +10 -0
  21. package/build/facet.d.ts.map +1 -0
  22. package/build/facet.js +16 -0
  23. package/build/facet.js.map +1 -0
  24. package/build/gate/consts.local.d.ts +2 -0
  25. package/build/gate/consts.local.d.ts.map +1 -0
  26. package/build/gate/consts.local.js +3 -0
  27. package/build/gate/consts.local.js.map +1 -0
  28. package/build/gate/index.d.ts +4 -0
  29. package/build/gate/index.d.ts.map +1 -0
  30. package/build/gate/index.js +4 -0
  31. package/build/gate/index.js.map +1 -0
  32. package/build/gate/param.d.ts +6 -0
  33. package/build/gate/param.d.ts.map +1 -0
  34. package/build/gate/param.js +83 -0
  35. package/build/gate/param.js.map +1 -0
  36. package/build/gate/resolve.d.ts +10 -0
  37. package/build/gate/resolve.d.ts.map +1 -0
  38. package/build/gate/resolve.js +93 -0
  39. package/build/gate/resolve.js.map +1 -0
  40. package/build/gate/types.d.ts +109 -0
  41. package/build/gate/types.d.ts.map +1 -0
  42. package/build/gate/types.js +2 -0
  43. package/build/gate/types.js.map +1 -0
  44. package/build/gate/types.local.d.ts +5 -0
  45. package/build/gate/types.local.d.ts.map +1 -0
  46. package/build/gate/types.local.js +2 -0
  47. package/build/gate/types.local.js.map +1 -0
  48. package/build/gate/validate.d.ts +4 -0
  49. package/build/gate/validate.d.ts.map +1 -0
  50. package/build/gate/validate.js +100 -0
  51. package/build/gate/validate.js.map +1 -0
  52. package/build/index.d.ts +9 -0
  53. package/build/index.d.ts.map +1 -1
  54. package/build/index.js +5 -0
  55. package/build/index.js.map +1 -1
  56. package/build/permission/types.d.ts +35 -0
  57. package/build/permission/types.d.ts.map +1 -0
  58. package/build/permission/types.js +2 -0
  59. package/build/permission/types.js.map +1 -0
  60. package/build/permission.d.ts +8 -0
  61. package/build/permission.d.ts.map +1 -0
  62. package/build/permission.js +34 -0
  63. package/build/permission.js.map +1 -0
  64. package/build/runtime/types.d.ts +90 -0
  65. package/build/runtime/types.d.ts.map +1 -0
  66. package/build/runtime/types.js +2 -0
  67. package/build/runtime/types.js.map +1 -0
  68. package/build/runtime.d.ts +75 -0
  69. package/build/runtime.d.ts.map +1 -0
  70. package/build/runtime.js +75 -0
  71. package/build/runtime.js.map +1 -0
  72. package/build/schemas.d.ts +13 -0
  73. package/build/schemas.d.ts.map +1 -0
  74. package/build/schemas.js +108 -0
  75. package/build/schemas.js.map +1 -0
  76. package/build/types.d.ts +429 -30
  77. package/build/types.d.ts.map +1 -1
  78. package/build/utils/types.d.ts +12 -0
  79. package/build/utils/types.d.ts.map +1 -0
  80. package/build/utils/types.js +2 -0
  81. package/build/utils/types.js.map +1 -0
  82. package/build/utils.d.ts +7 -6
  83. package/build/utils.d.ts.map +1 -1
  84. package/build/utils.js +10 -1
  85. package/build/utils.js.map +1 -1
  86. package/package.json +12 -7
  87. package/src/consts.local.ts +26 -0
  88. package/src/consts.ts +241 -1
  89. package/src/errors.ts +44 -0
  90. package/src/facet.ts +17 -0
  91. package/src/gate/consts.local.ts +3 -0
  92. package/src/gate/index.ts +3 -0
  93. package/src/gate/param.ts +108 -0
  94. package/src/gate/resolve.ts +114 -0
  95. package/src/gate/types.local.ts +4 -0
  96. package/src/gate/types.ts +117 -0
  97. package/src/gate/validate.ts +123 -0
  98. package/src/index.ts +9 -0
  99. package/src/permission/types.ts +33 -0
  100. package/src/permission.ts +48 -0
  101. package/src/runtime/types.ts +107 -0
  102. package/src/runtime.ts +142 -0
  103. package/src/schemas.ts +122 -0
  104. package/src/types.ts +492 -31
  105. package/src/utils/types.ts +11 -0
  106. package/src/utils.ts +12 -7
  107. package/tests/gate-param.spec.ts +233 -0
  108. package/tests/runtime.spec.ts +101 -0
  109. package/tests/tsconfig.json +12 -0
  110. package/tests/utils.spec.ts +47 -1
@@ -0,0 +1,117 @@
1
+ import type { GateParamSource, GateParamErrorCode, GateResolutionFailure } from '../consts.js'
2
+
3
+ /**
4
+ * The parts of a request a gate selector may read.
5
+ *
6
+ * Structural on purpose: `AbstractRequest` is assignable to it, so `@owlmeans/server-iam` passes its
7
+ * request straight through, while tooling outside the server stack can resolve a selector against a
8
+ * plain object without taking a dependency on `@owlmeans/entrypoint`.
9
+ */
10
+ export interface GateRequestLike {
11
+ params?: unknown
12
+ query?: unknown
13
+ body?: unknown
14
+ headers?: unknown
15
+ auth?: unknown
16
+ }
17
+
18
+ /** Where one gate param reads its resource id from. */
19
+ export interface GateResourceSelector {
20
+ /** The selector text exactly as written, for diagnostics. */
21
+ readonly selector: string
22
+ /** Undefined for the bare form, which searches `sources` in order. */
23
+ readonly source?: GateParamSource
24
+ /** Length 1 and UNSPLIT for the bare form — a bare key may legally contain dots. */
25
+ readonly path: string[]
26
+ /** The explicit source, or `DEFAULT_GATE_PARAM_SOURCES`. */
27
+ readonly sources: GateParamSource[]
28
+ }
29
+
30
+ export interface GateParamProblem {
31
+ code: GateParamErrorCode
32
+ detail: string
33
+ }
34
+
35
+ export interface ParsedGateParam {
36
+ permission: string
37
+ /**
38
+ * @deprecated The bare-form flat key, kept so existing readers keep compiling. Read `resource`,
39
+ * which carries the source and the path for both forms.
40
+ */
41
+ resourceParam?: string
42
+ resource?: GateResourceSelector
43
+ error?: GateParamProblem
44
+ }
45
+
46
+ export interface GateResourceResolution {
47
+ id?: string
48
+ from?: GateParamSource
49
+ reason?: GateResolutionFailure
50
+ }
51
+
52
+ /** What an entrypoint declares, so a selector can be checked against it before it is ever deployed. */
53
+ export interface GateParamAudit {
54
+ routePath?: string
55
+ routeParams?: string[]
56
+ filter?: {
57
+ query?: object
58
+ params?: object
59
+ body?: object
60
+ headers?: object
61
+ }
62
+ }
63
+
64
+ export interface GateParamIssue {
65
+ param: string
66
+ code: GateParamErrorCode
67
+ detail: string
68
+ }
69
+
70
+ /** The gate-param grammar: `<permission>@<selector>` read, written and taken apart. */
71
+ export interface GateParamHelper {
72
+ /**
73
+ * Read the `@…` half of a gate param.
74
+ *
75
+ * Two forms, and the difference between them is deliberate:
76
+ *
77
+ * `@enquiryId` — bare. A FLAT key, searched in `params` then `query`. It is never split on
78
+ * a dot, because a query key legally contains one and splitting it would
79
+ * change the meaning of selectors already deployed.
80
+ * `@body:order.id` — qualified. An explicit source and a nested path.
81
+ *
82
+ * The rule worth remembering: the bare form is lenient and flat, the qualified form is strict and
83
+ * nested.
84
+ */
85
+ parseGateSelector: (selector: string) => GateResourceSelector | GateParamProblem
86
+ /**
87
+ * Split a gate param into the permission it checks and where the resource id comes from.
88
+ *
89
+ * Splits at the FIRST `@`. Everything before it is the permission name looked up in the subject's
90
+ * grants; everything after says where to read the resource id at request time. The suffix is the
91
+ * gate's syntax and nothing else's — a permission stored or granted under a name containing one is a
92
+ * key nothing ever looks up.
93
+ */
94
+ parseGateParam: (param: string) => ParsedGateParam
95
+ /** Compose a gate param. The inverse of `parseGateParam` for every selector it accepts. */
96
+ formatGateParam: (
97
+ permission: string,
98
+ selector?: string | { source?: GateParamSource, path: string[] }
99
+ ) => string
100
+ }
101
+
102
+ /** Gate params checked against the entrypoint that declares them. */
103
+ export interface GateValidationHelper {
104
+ /** The `:name` segments of a route template. */
105
+ routeParamsOf: (path: string) => string[]
106
+ /**
107
+ * Check gate params against what their entrypoint declares.
108
+ *
109
+ * Reports only what is provable from the declaration itself. Anything that would need a guess is
110
+ * left alone: a check that fires on correct code stops being read.
111
+ *
112
+ * Meant to run where a mistake can still be fixed — while code is being generated, or once per alias
113
+ * as a diagnostic. It must never decide a request: with `some()` semantics across params, one
114
+ * malformed sibling must not be able to refuse a legitimately passing one.
115
+ */
116
+ validateGateParams: (params: string[], audit?: GateParamAudit) => GateParamIssue[]
117
+ }
@@ -0,0 +1,123 @@
1
+ import { normalizePath, SEP, PARAM } from '@owlmeans/route'
2
+ import { GateParamSource, GateParamErrorCode } from '../consts.js'
3
+ import type { GateParamAudit, GateParamIssue, GateResourceSelector, GateValidationHelper } from './types.js'
4
+ import { gateParamHelper } from './param.js'
5
+ import type { SchemaLike } from './types.local.js'
6
+
7
+ export const createGateValidationHelper = (): GateValidationHelper => {
8
+ const routeParamsOf = (path: string): string[] =>
9
+ normalizePath(path).split(SEP)
10
+ .filter(segment => segment.startsWith(PARAM))
11
+ .map(segment => segment.slice(PARAM.length))
12
+
13
+ /**
14
+ * Whether a key can survive validation to reach the gate.
15
+ *
16
+ * The server compiles entrypoint filters with AJV's `removeAdditional`, which strips an undeclared
17
+ * key BEFORE the handler — and therefore before the gate — runs. So a selector naming a key the
18
+ * filter does not declare denies every request, with a clean build and nothing logged.
19
+ *
20
+ * The `additionalProperties === false` qualifier is what makes this check usable rather than noise:
21
+ * AJV only strips when a schema says so. An entrypoint that declares no filter for a source strips
22
+ * nothing, and reporting those would fire on nearly every endpoint and be ignored within a week.
23
+ */
24
+ const unreachable = (schema: object | undefined, key: string): boolean => {
25
+ if (schema == null) {
26
+ return false
27
+ }
28
+
29
+ const { properties, additionalProperties } = schema as SchemaLike
30
+ if (additionalProperties !== false) {
31
+ return false
32
+ }
33
+
34
+ return properties == null || !(key in properties)
35
+ }
36
+
37
+ const filterFor = (audit: GateParamAudit, source: GateParamSource): object | undefined => {
38
+ switch (source) {
39
+ case GateParamSource.Query: return audit.filter?.query
40
+ case GateParamSource.Body: return audit.filter?.body
41
+ case GateParamSource.Headers: return audit.filter?.headers
42
+ case GateParamSource.Params: return audit.filter?.params
43
+ case GateParamSource.Auth: return undefined
44
+ }
45
+ }
46
+
47
+ const auditSelector = (
48
+ audit: GateParamAudit, selector: GateResourceSelector
49
+ ): Omit<GateParamIssue, 'param'> | null => {
50
+ const key = selector.path[0]
51
+ if (key == null) {
52
+ return null
53
+ }
54
+
55
+ // The bare form searches route params first and the query second, so it is only provably wrong
56
+ // when BOTH are impossible. A declared route param settles it; otherwise fall through to the
57
+ // query check below, which only fires on an explicitly closed schema.
58
+ const routeParams = audit.routeParams
59
+ ?? (audit.routePath != null ? routeParamsOf(audit.routePath) : undefined)
60
+
61
+ if (selector.source == null) {
62
+ if (routeParams?.includes(key) === true) {
63
+ return null
64
+ }
65
+ if (unreachable(audit.filter?.query, key) && unreachable(audit.filter?.params, key)) {
66
+ return {
67
+ code: GateParamErrorCode.UnknownRouteParam,
68
+ detail: `"${key}" is not a route param of "${audit.routePath ?? '?'}"`
69
+ + ' and is declared by neither the params nor the query filter'
70
+ }
71
+ }
72
+ return null
73
+ }
74
+
75
+ if (selector.source === GateParamSource.Params) {
76
+ if (routeParams != null && !routeParams.includes(key) && unreachable(audit.filter?.params, key)) {
77
+ return {
78
+ code: GateParamErrorCode.UnknownRouteParam,
79
+ detail: `"${key}" is not a route param of "${audit.routePath ?? '?'}"`
80
+ }
81
+ }
82
+ return null
83
+ }
84
+
85
+ if (unreachable(filterFor(audit, selector.source), key)) {
86
+ return {
87
+ code: GateParamErrorCode.UnreachableKey,
88
+ detail: `the ${selector.source} filter declares no "${key}" and forbids additional properties,`
89
+ + ' so it is removed by validation before the gate can read it'
90
+ }
91
+ }
92
+
93
+ return null
94
+ }
95
+
96
+ const validateGateParams = (params: string[], audit?: GateParamAudit): GateParamIssue[] => {
97
+ const issues: GateParamIssue[] = []
98
+
99
+ for (const param of params) {
100
+ const parsed = gateParamHelper.parseGateParam(param)
101
+
102
+ if (parsed.error != null) {
103
+ issues.push({ param, code: parsed.error.code, detail: parsed.error.detail })
104
+ continue
105
+ }
106
+
107
+ if (parsed.resource == null || audit == null) {
108
+ continue
109
+ }
110
+
111
+ const issue = auditSelector(audit, parsed.resource)
112
+ if (issue != null) {
113
+ issues.push({ param, ...issue })
114
+ }
115
+ }
116
+
117
+ return issues
118
+ }
119
+
120
+ return { routeParamsOf, validateGateParams }
121
+ }
122
+
123
+ export const gateValidationHelper = createGateValidationHelper()
package/src/index.ts CHANGED
@@ -1,4 +1,13 @@
1
1
  export type * from './types.js'
2
+ export type * from './gate/types.js'
3
+ export type * from './permission/types.js'
4
+ export type * from './runtime/types.js'
5
+ export type * from './utils/types.js'
2
6
  export * from './consts.js'
3
7
  export * from './errors.js'
4
8
  export * from './utils.js'
9
+ export * from './facet.js'
10
+ export * from './gate/index.js'
11
+ export * from './permission.js'
12
+ export * from './runtime.js'
13
+ export { IamRuntimeOrganizationSchema, IamRuntimeOrganizationListSchema, IamRuntimeMemberSchema, IamRuntimeMemberListSchema, IamRuntimeMemberUpdateSchema, IamRuntimePermissionSchema, IamRuntimePermissionListSchema, IamRuntimeGrantSchema, IamRuntimeGrantListSchema, IamRuntimeGrantRequestSchema } from './schemas.js'
@@ -0,0 +1,33 @@
1
+ import type { GateParamProblem } from '../gate/types.js'
2
+
3
+ /** A permission NAME taken apart. Never carries a selector — `@` is the gate's syntax alone. */
4
+ export interface ParsedPermissionName {
5
+ /** The whole name, exactly as it must be stored and granted. */
6
+ name: string
7
+ /** What IAM stores as `resource`. The whole name when there is no action separator. */
8
+ resource: string
9
+ /** What IAM stores as `action`. Absent for a bare, unsplittable legacy name. */
10
+ action?: string
11
+ /** Set when the argument carried an `@` — which a NAME never legally does. */
12
+ problem?: GateParamProblem
13
+ }
14
+
15
+ /** Permission NAMES: `<resource>--<action>`, never carrying a gate selector. */
16
+ export interface PermissionNameHelper {
17
+ /**
18
+ * Read a permission NAME.
19
+ *
20
+ * The name is `<resource>--<action>`, with TWO hyphens. One hyphen is not a separator: `enquiry-view`
21
+ * is a resource called `enquiry-view` carrying no action, and it must keep parsing that way — a name
22
+ * already granted somewhere cannot be re-interpreted without orphaning the grant.
23
+ *
24
+ * A name never carries a gate selector. `@` is the gate's syntax, and a name containing one is
25
+ * exactly the corruption this function exists to make visible, so it is reported through `problem`
26
+ * rather than quietly split.
27
+ */
28
+ parsePermissionName: (name: string) => ParsedPermissionName
29
+ /** Compose a permission name. The inverse of `parsePermissionName`. */
30
+ composePermissionName: (parts: { resource: string, action?: string }) => string
31
+ /** True when the value is a usable permission name — well-formed and carrying no gate selector. */
32
+ isPermissionName: (value: string) => boolean
33
+ }
@@ -0,0 +1,48 @@
1
+ import { PERMISSION_ACTION_SEPARATOR, RESOURCE_PARAM_SEPARATOR, GateParamErrorCode } from './consts.js'
2
+ import type { ParsedPermissionName, PermissionNameHelper } from './permission/types.js'
3
+
4
+ export const createPermissionNameHelper = (): PermissionNameHelper => {
5
+ const parsePermissionName = (name: string): ParsedPermissionName => {
6
+ const problem = name.includes(RESOURCE_PARAM_SEPARATOR)
7
+ ? {
8
+ code: GateParamErrorCode.UnreachableKey,
9
+ detail: `"${name}" carries a gate selector; "${RESOURCE_PARAM_SEPARATOR}" is never part of a`
10
+ + ' permission name, and nothing ever looks up a name that contains one'
11
+ }
12
+ : undefined
13
+
14
+ const idx = name.indexOf(PERMISSION_ACTION_SEPARATOR)
15
+ if (idx < 0) {
16
+ return { name, resource: name, ...(problem != null ? { problem } : {}) }
17
+ }
18
+
19
+ const action = name.slice(idx + PERMISSION_ACTION_SEPARATOR.length)
20
+
21
+ return {
22
+ name,
23
+ resource: name.slice(0, idx),
24
+ ...(action !== '' ? { action } : {}),
25
+ ...(problem != null ? { problem } : {})
26
+ }
27
+ }
28
+
29
+ const composePermissionName = (
30
+ parts: { resource: string, action?: string }
31
+ ): string => parts.action == null || parts.action === ''
32
+ ? parts.resource
33
+ : `${parts.resource}${PERMISSION_ACTION_SEPARATOR}${parts.action}`
34
+
35
+ const isPermissionName = (value: string): boolean =>
36
+ value !== '' && !value.includes(RESOURCE_PARAM_SEPARATOR)
37
+
38
+ return { parsePermissionName, composePermissionName, isPermissionName }
39
+ }
40
+
41
+ export const permissionNameHelper = createPermissionNameHelper()
42
+
43
+ /** @deprecated compat:factory-refactor — use `permissionNameHelper.parsePermissionName(…)` */
44
+ export const parsePermissionName = (name: string): ParsedPermissionName =>
45
+ permissionNameHelper.parsePermissionName(name)
46
+
47
+ /** @deprecated compat:factory-refactor — use `permissionNameHelper.isPermissionName(…)` */
48
+ export const isPermissionName = (value: string): boolean => permissionNameHelper.isPermissionName(value)
@@ -0,0 +1,107 @@
1
+ import type { IamDefaultClass, IamGrantMode, IamGrantOrigin } from '../consts.js'
2
+
3
+ /** One organization of the calling subject. */
4
+ export interface IamRuntimeOrganization {
5
+ entitySlug: string
6
+ title?: string
7
+ owner: boolean
8
+ groups?: string[]
9
+ home?: boolean
10
+ }
11
+
12
+ export interface IamRuntimeOrganizationList {
13
+ items: IamRuntimeOrganization[]
14
+ }
15
+
16
+ export interface IamRuntimeOrganizationCreate {
17
+ title?: string
18
+ }
19
+
20
+ export interface IamRuntimeOrganizationUpdate {
21
+ title: string
22
+ }
23
+
24
+ export interface IamRuntimeOrganizationParams {
25
+ entitySlug: string
26
+ }
27
+
28
+ export interface IamRuntimeMember {
29
+ profileId: string
30
+ email?: string
31
+ name?: string
32
+ owner: boolean
33
+ groups: string[]
34
+ managed?: boolean
35
+ }
36
+
37
+ export interface IamRuntimeMemberList {
38
+ items: IamRuntimeMember[]
39
+ }
40
+
41
+ /** Find-or-create by e-mail; adding someone twice answers the same member. */
42
+ export interface IamRuntimeMemberInvite {
43
+ email: string
44
+ name?: string
45
+ owner?: boolean
46
+ }
47
+
48
+ export interface IamRuntimeMemberUpdate {
49
+ owner?: boolean
50
+ groups?: string[]
51
+ }
52
+
53
+ export interface IamRuntimeMemberParams {
54
+ entitySlug: string
55
+ profileId: string
56
+ }
57
+
58
+ /** A definition of the calling client that can be granted in an organization. */
59
+ export interface IamRuntimePermission {
60
+ name: string
61
+ title?: string
62
+ area?: string
63
+ resourceScoped?: boolean
64
+ managed?: boolean
65
+ defaultClass?: IamDefaultClass
66
+ }
67
+
68
+ export interface IamRuntimePermissionList {
69
+ items: IamRuntimePermission[]
70
+ }
71
+
72
+ /** A grant of a member in the organization; `through` names the group a `Group`-origin one comes from. */
73
+ export interface IamRuntimeGrant {
74
+ profileId: string
75
+ permission: string
76
+ resources?: string[]
77
+ mode?: IamGrantMode
78
+ origin?: IamGrantOrigin
79
+ through?: string
80
+ }
81
+
82
+ export interface IamRuntimeGrantList {
83
+ items: IamRuntimeGrant[]
84
+ }
85
+
86
+ export interface IamRuntimeGrantQuery {
87
+ profileId?: string
88
+ }
89
+
90
+ /** Grant or revoke. The binding is the path's organization — the body cannot name another. */
91
+ export interface IamRuntimeGrantRequest {
92
+ profileId: string
93
+ permission: string
94
+ resources?: string[]
95
+ mode?: IamGrantMode
96
+ }
97
+
98
+ export interface IamRuntimeAck {
99
+ ok: boolean
100
+ }
101
+
102
+ export interface IamRuntimeProtocolOptions {
103
+ /** The service alias that serves the API — every route, the base included, is pinned to it. */
104
+ service: string
105
+ /** The base path under that service. Defaults to `IAM_RUNTIME_PATH`. */
106
+ path?: string
107
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,142 @@
1
+ import { contract, openProtocol, protocol, typed, type EntrypointProtocol, type OpenRequest, type OpenValue } from '@owlmeans/entrypoint'
2
+ import { backend, route, RouteMethod } from '@owlmeans/route'
3
+ import { IAM_RUNTIME_GUARD, IAM_RUNTIME_PATH, IAM_RUNTIME_ROUTES, IamRuntimeAckSchema, IamRuntimeGrantQuerySchema, IamRuntimeMemberInviteSchema, IamRuntimeMemberParamsSchema, IamRuntimeOrganizationCreateSchema, IamRuntimeOrganizationParamsSchema, IamRuntimeOrganizationUpdateSchema } from './consts.js'
4
+ import { aliases } from './consts.local.js'
5
+ import type { IamRuntimeAck, IamRuntimeGrant, IamRuntimeGrantList, IamRuntimeGrantQuery, IamRuntimeGrantRequest, IamRuntimeMember, IamRuntimeMemberInvite, IamRuntimeMemberList, IamRuntimeMemberParams, IamRuntimeMemberUpdate, IamRuntimeOrganization, IamRuntimeOrganizationCreate, IamRuntimeOrganizationList, IamRuntimeOrganizationParams, IamRuntimeOrganizationUpdate, IamRuntimePermissionList, IamRuntimeProtocolOptions } from './runtime/types.js'
6
+ import { IamRuntimeOrganizationSchema, IamRuntimeOrganizationListSchema, IamRuntimeMemberSchema, IamRuntimeMemberListSchema, IamRuntimeMemberUpdateSchema, IamRuntimePermissionListSchema, IamRuntimeGrantSchema, IamRuntimeGrantListSchema, IamRuntimeGrantRequestSchema } from './schemas.js'
7
+
8
+ // ---------------------------------------------------------------------------------------------
9
+ // Schemas — closed throughout: the server's AJV strips an undeclared key silently, so a field a
10
+ // caller sends and the schema omits would vanish without an error.
11
+ // ---------------------------------------------------------------------------------------------
12
+ // ---------------------------------------------------------------------------------------------
13
+ // The protocol tree
14
+ // ---------------------------------------------------------------------------------------------
15
+
16
+ // Kept as a type: the inferred shape of the declareRuntime protocol tree, so it cannot leave this file.
17
+ export type IamRuntimeProtocols = ReturnType<typeof declareRuntime>
18
+
19
+ const declareRuntime = (opts: IamRuntimeProtocolOptions) => {
20
+ const { service } = opts
21
+ const base: EntrypointProtocol<OpenRequest, OpenValue> = openProtocol(
22
+ route(aliases.base, opts.path ?? IAM_RUNTIME_PATH, backend({ service })),
23
+ { guards: IAM_RUNTIME_GUARD },
24
+ )
25
+ // A route's service is never inherited from its parent, so each leaf names it too: a leaf left
26
+ // unpinned would be mounted by every process that happens to bind the tree.
27
+ const leaf = (alias: string, path: string, method: RouteMethod) =>
28
+ route(alias, path, backend({ parent: base, service }, method))
29
+
30
+ return Object.freeze({
31
+ base,
32
+
33
+ organizations: Object.freeze({
34
+ /** Any subject: its own organizations. */
35
+ list: protocol(
36
+ leaf(aliases.organization.list, IAM_RUNTIME_ROUTES.organizations, RouteMethod.GET),
37
+ contract(typed<IamRuntimeOrganizationList>(IamRuntimeOrganizationListSchema)),
38
+ ),
39
+ /** Any subject of a tenanted client; the caller becomes its owner. */
40
+ create: protocol(
41
+ leaf(aliases.organization.create, IAM_RUNTIME_ROUTES.organizations, RouteMethod.POST),
42
+ contract.request(
43
+ { body: typed<IamRuntimeOrganizationCreate>(IamRuntimeOrganizationCreateSchema) },
44
+ typed<IamRuntimeOrganization>(IamRuntimeOrganizationSchema),
45
+ ),
46
+ ),
47
+ /** Owner. */
48
+ update: protocol(
49
+ leaf(aliases.organization.update, IAM_RUNTIME_ROUTES.organization, RouteMethod.POST),
50
+ contract.request({
51
+ params: typed<IamRuntimeOrganizationParams>(IamRuntimeOrganizationParamsSchema),
52
+ body: typed<IamRuntimeOrganizationUpdate>(IamRuntimeOrganizationUpdateSchema),
53
+ }, typed<IamRuntimeOrganization>(IamRuntimeOrganizationSchema)),
54
+ ),
55
+ }),
56
+
57
+ members: Object.freeze({
58
+ /** Member. */
59
+ list: protocol(
60
+ leaf(aliases.member.list, IAM_RUNTIME_ROUTES.members, RouteMethod.GET),
61
+ contract.request(
62
+ { params: typed<IamRuntimeOrganizationParams>(IamRuntimeOrganizationParamsSchema) },
63
+ typed<IamRuntimeMemberList>(IamRuntimeMemberListSchema),
64
+ ),
65
+ ),
66
+ /** Owner. Find-or-create by e-mail, idempotent. */
67
+ add: protocol(
68
+ leaf(aliases.member.add, IAM_RUNTIME_ROUTES.members, RouteMethod.POST),
69
+ contract.request({
70
+ params: typed<IamRuntimeOrganizationParams>(IamRuntimeOrganizationParamsSchema),
71
+ body: typed<IamRuntimeMemberInvite>(IamRuntimeMemberInviteSchema),
72
+ }, typed<IamRuntimeMember>(IamRuntimeMemberSchema)),
73
+ ),
74
+ /** Owner. */
75
+ update: protocol(
76
+ leaf(aliases.member.update, IAM_RUNTIME_ROUTES.member, RouteMethod.POST),
77
+ contract.request({
78
+ params: typed<IamRuntimeMemberParams>(IamRuntimeMemberParamsSchema),
79
+ body: typed<IamRuntimeMemberUpdate>(IamRuntimeMemberUpdateSchema),
80
+ }, typed<IamRuntimeMember>(IamRuntimeMemberSchema)),
81
+ ),
82
+ /** Owner, or the member itself. */
83
+ remove: protocol(
84
+ leaf(aliases.member.remove, IAM_RUNTIME_ROUTES.memberRemove, RouteMethod.POST),
85
+ contract.request(
86
+ { params: typed<IamRuntimeMemberParams>(IamRuntimeMemberParamsSchema) },
87
+ typed<IamRuntimeAck>(IamRuntimeAckSchema),
88
+ ),
89
+ ),
90
+ }),
91
+
92
+ permissions: Object.freeze({
93
+ /** Member. */
94
+ list: protocol(
95
+ leaf(aliases.permission.list, IAM_RUNTIME_ROUTES.permissions, RouteMethod.GET),
96
+ contract.request(
97
+ { params: typed<IamRuntimeOrganizationParams>(IamRuntimeOrganizationParamsSchema) },
98
+ typed<IamRuntimePermissionList>(IamRuntimePermissionListSchema),
99
+ ),
100
+ ),
101
+ }),
102
+
103
+ grants: Object.freeze({
104
+ /** Owner. */
105
+ list: protocol(
106
+ leaf(aliases.grant.list, IAM_RUNTIME_ROUTES.grants, RouteMethod.GET),
107
+ contract.request({
108
+ params: typed<IamRuntimeOrganizationParams>(IamRuntimeOrganizationParamsSchema),
109
+ query: typed<IamRuntimeGrantQuery>(IamRuntimeGrantQuerySchema),
110
+ }, typed<IamRuntimeGrantList>(IamRuntimeGrantListSchema)),
111
+ ),
112
+ /** Owner. */
113
+ assign: protocol(
114
+ leaf(aliases.grant.assign, IAM_RUNTIME_ROUTES.grants, RouteMethod.POST),
115
+ contract.request({
116
+ params: typed<IamRuntimeOrganizationParams>(IamRuntimeOrganizationParamsSchema),
117
+ body: typed<IamRuntimeGrantRequest>(IamRuntimeGrantRequestSchema),
118
+ }, typed<IamRuntimeGrant>(IamRuntimeGrantSchema)),
119
+ ),
120
+ /** Owner. */
121
+ revoke: protocol(
122
+ leaf(aliases.grant.revoke, IAM_RUNTIME_ROUTES.grantsRevoke, RouteMethod.POST),
123
+ contract.request({
124
+ params: typed<IamRuntimeOrganizationParams>(IamRuntimeOrganizationParamsSchema),
125
+ body: typed<IamRuntimeGrantRequest>(IamRuntimeGrantRequestSchema),
126
+ }, typed<IamRuntimeAck>(IamRuntimeAckSchema)),
127
+ ),
128
+ }),
129
+ })
130
+ }
131
+
132
+ /**
133
+ * Declare the runtime IAM API — the provider-side surface a tenanted client's server calls on behalf
134
+ * of its signed-in subject: the subject's organizations, their members, the grantable definitions
135
+ * and the grants inside one organization.
136
+ *
137
+ * Every leaf hangs under ONE base carrying `IAM_RUNTIME_GUARD`, so none can be reached without the
138
+ * provider's own access token; the caller's client and account come from that token, the
139
+ * organization from the path. Owner/member checks, the tenancy-config gate and the write rules are
140
+ * the serving process's — a declaration decides nothing.
141
+ */
142
+ export const makeIamRuntimeProtocols = (opts: IamRuntimeProtocolOptions): IamRuntimeProtocols => declareRuntime(opts)