@owlmeans/iam 0.1.18-rc.0 → 0.1.18-rc.11
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/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/iam/SKILL.md +50 -0
- package/build/consts.d.ts +91 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +97 -0
- package/build/consts.js.map +1 -1
- package/build/errors.d.ts +13 -0
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +17 -0
- package/build/errors.js.map +1 -1
- package/build/gate/index.d.ts +4 -0
- package/build/gate/index.d.ts.map +1 -0
- package/build/gate/index.js +4 -0
- package/build/gate/index.js.map +1 -0
- package/build/gate/param.d.ts +31 -0
- package/build/gate/param.d.ts.map +1 -0
- package/build/gate/param.js +99 -0
- package/build/gate/param.js.map +1 -0
- package/build/gate/resolve.d.ts +10 -0
- package/build/gate/resolve.d.ts.map +1 -0
- package/build/gate/resolve.js +93 -0
- package/build/gate/resolve.js.map +1 -0
- package/build/gate/validate.d.ts +15 -0
- package/build/gate/validate.d.ts.map +1 -0
- package/build/gate/validate.js +107 -0
- package/build/gate/validate.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/build/permission.d.ts +21 -0
- package/build/permission.d.ts.map +1 -0
- package/build/permission.js +39 -0
- package/build/permission.js.map +1 -0
- package/build/types.d.ts +251 -6
- package/build/types.d.ts.map +1 -1
- package/package.json +6 -5
- package/src/consts.ts +105 -0
- package/src/errors.ts +19 -0
- package/src/gate/index.ts +3 -0
- package/src/gate/param.ts +122 -0
- package/src/gate/resolve.ts +114 -0
- package/src/gate/validate.ts +132 -0
- package/src/index.ts +2 -0
- package/src/permission.ts +48 -0
- package/src/types.ts +283 -6
- package/tests/gate-param.spec.ts +234 -0
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/iam",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-08-
|
|
4
|
+
"version": "0.1.18-rc.9",
|
|
5
|
+
"generatedAt": "2026-08-27T16:32:18.499Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -23,6 +23,9 @@ Provider-agnostic IAM abstraction. Defines `IamService` interface and related ty
|
|
|
23
23
|
| `IamPermissionDefinition` | type | Declared permission `{ name, resource, action?, resourceScoped?, title? }` |
|
|
24
24
|
| `IamGrantArgs` | type | `{ resources?: string[] }` — present = resource-scoped grant form |
|
|
25
25
|
| `IamGrant` | type | `{ profileId, clientId, permission, resources? }` |
|
|
26
|
+
| `IamUser` | type | End-user of an entity `{ profileId, email?, name?, role, disabled?, grantCount? }` |
|
|
27
|
+
| `IamUserInvite` | type | `{ email, name?, role? }` — find-or-create args |
|
|
28
|
+
| `IamUserUpdate` | type | `{ name?, role?, disabled? }` |
|
|
26
29
|
| `hasPermission` | fn | `(auth, permission, { scope?, resourceId? }?)` — checks `Authorization.permissions` PermissionSet[]; an unscoped set satisfies a resourceId check |
|
|
27
30
|
| `DEFAULT_ALIAS` | const | Default service alias `'iam-service'` |
|
|
28
31
|
| `IAM_MODE_KEYCLOAK` | const | `'keycloak'` |
|
|
@@ -62,8 +65,15 @@ interface IamService extends InitializedService {
|
|
|
62
65
|
|
|
63
66
|
// Provision an OIDC client for a tenant and return its credentials.
|
|
64
67
|
// options.redirectUris hardens the client; always pass them (see "Issuer & redirect URIs").
|
|
68
|
+
// MUST refuse an existing record owned by another entity — see "A client id is a global name".
|
|
65
69
|
ensureClient: (entityId: string, clientId: string, options?: IamClientOptions) => Promise<IamClient>
|
|
66
70
|
|
|
71
|
+
// Reserve a free client id without provisioning it; false when anyone else holds it.
|
|
72
|
+
claimClient: (entityId: string, clientId: string) => Promise<boolean>
|
|
73
|
+
|
|
74
|
+
// Release a client id and everything keyed by it (called when a project/slot is deleted).
|
|
75
|
+
deleteClient: (entityId: string, clientId: string) => Promise<void>
|
|
76
|
+
|
|
67
77
|
// Provision a named permission on a client resource; returns the resource name
|
|
68
78
|
ensurePermission: (entityId: string, clientId: string, resource?: string, args?: IamPermissionArgs) => Promise<string>
|
|
69
79
|
|
|
@@ -83,9 +93,49 @@ interface IamService extends InitializedService {
|
|
|
83
93
|
|
|
84
94
|
// List grants for the client, optionally for one subject
|
|
85
95
|
listGrants: (entityId: string, clientId: string, profileId?: string) => Promise<IamGrant[]>
|
|
96
|
+
|
|
97
|
+
// --- End-user management (customer-wide users, shared per entityId) ---
|
|
98
|
+
|
|
99
|
+
// Every end-user of the entity. clientId scopes the reported grantCount — it does NOT filter.
|
|
100
|
+
listUsers: (entityId: string, clientId?: string) => Promise<IamUser[]>
|
|
101
|
+
getUser: (entityId: string, profileId: string) => Promise<IamUser | null>
|
|
102
|
+
// Find-or-create by email — the one way an end-user record comes into existence.
|
|
103
|
+
inviteUser: (entityId: string, invite: IamUserInvite) => Promise<IamUser>
|
|
104
|
+
updateUser: (entityId: string, profileId: string, update: IamUserUpdate) => Promise<IamUser>
|
|
105
|
+
removeUser: (entityId: string, profileId: string) => Promise<void>
|
|
86
106
|
}
|
|
87
107
|
```
|
|
88
108
|
|
|
109
|
+
## End-users are customer-wide
|
|
110
|
+
|
|
111
|
+
An end-user belongs to the **entity**, not to a project: one customer's users are shared across every
|
|
112
|
+
project they own. `listUsers` therefore returns the entity's whole set and `clientId` only scopes the
|
|
113
|
+
reported `grantCount` to one project's client. Do not reintroduce a per-client filter — a person who
|
|
114
|
+
has authenticated against a project but holds no grant there is still that project's user, and the
|
|
115
|
+
screen that hands out grants is exactly where they have to be visible.
|
|
116
|
+
|
|
117
|
+
`inviteUser` is idempotent by email, which is what lets an invitation and a first sign-in converge on
|
|
118
|
+
one record instead of accumulating twins. Backends that have no user store of their own (keycloak,
|
|
119
|
+
where the customer manages realm users in its console) throw `IamUnsupported('user-management')` from
|
|
120
|
+
all five methods; callers turn that into an "external console" fallback rather than an error.
|
|
121
|
+
|
|
122
|
+
## A client id is a global name
|
|
123
|
+
|
|
124
|
+
A provider resolves a client from the bare `client_id` a relying party sends — the adapter gets no
|
|
125
|
+
tenant context — so the id is unique across the whole deployment, not per entity, and the store
|
|
126
|
+
cannot namespace it on a consumer's behalf. Uniqueness has to be *in the id*.
|
|
127
|
+
|
|
128
|
+
- Compose it with the owning entity in it. A name derived from a project's own name alone lets two
|
|
129
|
+
organizations share one registration: one secret, one redirect-URI list, one permission-definition
|
|
130
|
+
set, and one grant namespace, because `PermissionSet.scope` is that same string.
|
|
131
|
+
- `ensureClient` refuses a record belonging to another entity rather than returning it. The failure
|
|
132
|
+
is deliberate — a loud provisioning error is the only alternative to a silent cross-tenant handover.
|
|
133
|
+
- Assign an id **once** and reserve it with `claimClient`. Whatever a consumer derives ids from
|
|
134
|
+
(a project alias, a slug) may later be released and re-used by a sibling, so only the registry can
|
|
135
|
+
say whether an id is free. `deleteClient` gives it back, or a deleted project's name is burned.
|
|
136
|
+
- Backends whose clients are already per-tenant (keycloak realms) satisfy this by construction:
|
|
137
|
+
`claimClient` returns true and `deleteClient` may be `IamUnsupported`.
|
|
138
|
+
|
|
89
139
|
## `IamClient.realm` field
|
|
90
140
|
|
|
91
141
|
`realm` is set by the provider to the entity ID. Use it instead of accessing internal properties:
|
package/build/consts.d.ts
CHANGED
|
@@ -2,4 +2,95 @@ export declare const DEFAULT_ALIAS = "iam-service";
|
|
|
2
2
|
export declare const IAM_MODE_KEYCLOAK = "keycloak";
|
|
3
3
|
export declare const IAM_MODE_INTEGRATED = "integrated";
|
|
4
4
|
export type IamMode = typeof IAM_MODE_KEYCLOAK | typeof IAM_MODE_INTEGRATED;
|
|
5
|
+
/**
|
|
6
|
+
* Gate-param syntax: `<permission>[@<selector>]`.
|
|
7
|
+
*
|
|
8
|
+
* The `@` belongs to the GATE and to nothing else. It says where a resource id is read from at
|
|
9
|
+
* request time; it is never part of a permission's stored NAME. A definition or a grant whose name
|
|
10
|
+
* carries one is a key nothing ever looks up, so every grant against it is a silent no-op.
|
|
11
|
+
*/
|
|
12
|
+
export declare const RESOURCE_PARAM_SEPARATOR = "@";
|
|
13
|
+
/**
|
|
14
|
+
* Separates an explicit source from its path: `@body:order.id`.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately `:` rather than a dot. Under a dotted-only form the meaning of a selector changes the
|
|
17
|
+
* moment a route gains a param named like a source keyword — `/report/:query` with `@query.id` flips
|
|
18
|
+
* from "the route param literally named `query.id`" to "the query string's `id`", redirecting an
|
|
19
|
+
* authorization lookup from a router-matched value to attacker-supplied input, caused by an
|
|
20
|
+
* unrelated route rename. `:` is decidable in one character, and no gate-param string written before
|
|
21
|
+
* this existed can contain one.
|
|
22
|
+
*/
|
|
23
|
+
export declare const RESOURCE_SOURCE_SEPARATOR = ":";
|
|
24
|
+
/** Separates path segments inside a qualified selector: `@body:order.id`. */
|
|
25
|
+
export declare const RESOURCE_PATH_SEPARATOR = ".";
|
|
26
|
+
/** Where a qualified selector may read a resource id from. */
|
|
27
|
+
export declare enum GateParamSource {
|
|
28
|
+
Params = "params",
|
|
29
|
+
Query = "query",
|
|
30
|
+
Body = "body",
|
|
31
|
+
Headers = "headers",
|
|
32
|
+
Auth = "auth"
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* What the bare form (`@enquiryId`) searches, in order.
|
|
36
|
+
*
|
|
37
|
+
* Params first, then query. The query fallback is legacy and deliberately kept: removing it would
|
|
38
|
+
* silently deny already-deployed applications whose selector happens to name a query key, and a
|
|
39
|
+
* silent denial inside a customer's running app is the worst available outcome.
|
|
40
|
+
*/
|
|
41
|
+
export declare const DEFAULT_GATE_PARAM_SOURCES: GateParamSource[];
|
|
42
|
+
/** Why a gate param is structurally wrong — a misconfiguration, not a failed authorization. */
|
|
43
|
+
export declare enum GateParamErrorCode {
|
|
44
|
+
UnknownSource = "gate-param:unknown-source",
|
|
45
|
+
EmptySelector = "gate-param:empty-selector",
|
|
46
|
+
EmptySegment = "gate-param:empty-segment",
|
|
47
|
+
/** Named a request key the entrypoint's filter does not declare — AJV strips it before the gate. */
|
|
48
|
+
UnreachableKey = "gate-param:unreachable-key",
|
|
49
|
+
/** Named a route param the entrypoint's own path does not carry. */
|
|
50
|
+
UnknownRouteParam = "gate-param:unknown-route-param"
|
|
51
|
+
}
|
|
52
|
+
/** Why a selector produced no resource id. */
|
|
53
|
+
export declare enum GateResolutionFailure {
|
|
54
|
+
/** The source exists, the path does not. */
|
|
55
|
+
NotProvided = "not-provided",
|
|
56
|
+
/** The request carries no such source at all. */
|
|
57
|
+
SourceMissing = "source-missing",
|
|
58
|
+
/** Resolved to an object or a multi-element array — refused rather than stringified. */
|
|
59
|
+
NotScalar = "not-scalar",
|
|
60
|
+
/** The selector itself did not parse. */
|
|
61
|
+
Malformed = "malformed"
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Which FORM of a grant an operation addresses.
|
|
65
|
+
*
|
|
66
|
+
* A resource-scoped permission is grantable two ways, under one name: `Blanket` (a PermissionSet
|
|
67
|
+
* carrying no `resources`, which `hasPermission` reads as covering every id) and `Resources` (an
|
|
68
|
+
* explicit id list). They are stored separately and must be revoked separately, or a UI offering
|
|
69
|
+
* both controls would have one silently wipe the other.
|
|
70
|
+
*/
|
|
71
|
+
export declare enum IamGrantMode {
|
|
72
|
+
Blanket = "blanket",
|
|
73
|
+
Resources = "resources",
|
|
74
|
+
/** Revoke-only: every form. Illegal on a grant. */
|
|
75
|
+
All = "all"
|
|
76
|
+
}
|
|
77
|
+
/** What to do about grants that still reference a definition being removed. */
|
|
78
|
+
export declare enum IamRemovalPolicy {
|
|
79
|
+
/** Strip the permission from every subject of the entity, then drop the definition. */
|
|
80
|
+
Cascade = "cascade",
|
|
81
|
+
/** Refuse while any subject still holds it; the holders travel in the error. */
|
|
82
|
+
Refuse = "refuse"
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The grouping tiers a permission definition may be tagged with.
|
|
86
|
+
*
|
|
87
|
+
* A tag decides NOTHING at request time — `hasPermission` and every gate treat a permission name as
|
|
88
|
+
* one opaque string — and only groups the permission in an administrator's screen. `guest` is
|
|
89
|
+
* deliberately absent: a public surface declares no permissions, and adding an area later would
|
|
90
|
+
* re-classify names already tagged.
|
|
91
|
+
*/
|
|
92
|
+
export declare const IAM_AREAS: readonly ['user', 'operator', 'admin'];
|
|
93
|
+
export type IamArea = typeof IAM_AREAS[number];
|
|
94
|
+
/** Separates the resource from the action in a permission name. TWO hyphens, never one. */
|
|
95
|
+
export declare const PERMISSION_ACTION_SEPARATOR = "--";
|
|
5
96
|
//# sourceMappingURL=consts.d.ts.map
|
package/build/consts.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,gBAAgB,CAAA;AAE1C,eAAO,MAAM,iBAAiB,aAAa,CAAA;AAC3C,eAAO,MAAM,mBAAmB,eAAe,CAAA;AAE/C,MAAM,MAAM,OAAO,GAAG,OAAO,iBAAiB,GAAG,OAAO,mBAAmB,CAAA"}
|
|
1
|
+
{"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,gBAAgB,CAAA;AAE1C,eAAO,MAAM,iBAAiB,aAAa,CAAA;AAC3C,eAAO,MAAM,mBAAmB,eAAe,CAAA;AAE/C,MAAM,MAAM,OAAO,GAAG,OAAO,iBAAiB,GAAG,OAAO,mBAAmB,CAAA;AAE3E;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,MAAM,CAAA;AAE3C;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB,MAAM,CAAA;AAE5C,6EAA6E;AAC7E,eAAO,MAAM,uBAAuB,MAAM,CAAA;AAE1C,8DAA8D;AAC9D,oBAAY,eAAe;IACzB,MAAM,WAAW;IACjB,KAAK,UAAU;IACf,IAAI,SAAS;IACb,OAAO,YAAY;IACnB,IAAI,SAAS;CACd;AAED;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,EAAE,eAAe,EAEvD,CAAA;AAED,+FAA+F;AAC/F,oBAAY,kBAAkB;IAC5B,aAAa,8BAA8B;IAC3C,aAAa,8BAA8B;IAC3C,YAAY,6BAA6B;IACzC,oGAAoG;IACpG,cAAc,+BAA+B;IAC7C,oEAAoE;IACpE,iBAAiB,mCAAmC;CACrD;AAED,8CAA8C;AAC9C,oBAAY,qBAAqB;IAC/B,4CAA4C;IAC5C,WAAW,iBAAiB;IAC5B,iDAAiD;IACjD,aAAa,mBAAmB;IAChC,wFAAwF;IACxF,SAAS,eAAe;IACxB,yCAAyC;IACzC,SAAS,cAAc;CACxB;AAED;;;;;;;GAOG;AACH,oBAAY,YAAY;IACtB,OAAO,YAAY;IACnB,SAAS,cAAc;IACvB,mDAAmD;IACnD,GAAG,QAAQ;CACZ;AAED,+EAA+E;AAC/E,oBAAY,gBAAgB;IAC1B,uFAAuF;IACvF,OAAO,YAAY;IACnB,gFAAgF;IAChF,MAAM,WAAW;CAClB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,YAAI,MAAM,EAAE,UAAU,EAAE,OAAO,CAAU,CAAA;AAE/D,MAAM,MAAM,OAAO,GAAG,OAAO,SAAS,CAAC,MAAM,CAAC,CAAA;AAE9C,2FAA2F;AAC3F,eAAO,MAAM,2BAA2B,OAAO,CAAA"}
|
package/build/consts.js
CHANGED
|
@@ -1,4 +1,101 @@
|
|
|
1
1
|
export const DEFAULT_ALIAS = 'iam-service';
|
|
2
2
|
export const IAM_MODE_KEYCLOAK = 'keycloak';
|
|
3
3
|
export const IAM_MODE_INTEGRATED = 'integrated';
|
|
4
|
+
/**
|
|
5
|
+
* Gate-param syntax: `<permission>[@<selector>]`.
|
|
6
|
+
*
|
|
7
|
+
* The `@` belongs to the GATE and to nothing else. It says where a resource id is read from at
|
|
8
|
+
* request time; it is never part of a permission's stored NAME. A definition or a grant whose name
|
|
9
|
+
* carries one is a key nothing ever looks up, so every grant against it is a silent no-op.
|
|
10
|
+
*/
|
|
11
|
+
export const RESOURCE_PARAM_SEPARATOR = '@';
|
|
12
|
+
/**
|
|
13
|
+
* Separates an explicit source from its path: `@body:order.id`.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately `:` rather than a dot. Under a dotted-only form the meaning of a selector changes the
|
|
16
|
+
* moment a route gains a param named like a source keyword — `/report/:query` with `@query.id` flips
|
|
17
|
+
* from "the route param literally named `query.id`" to "the query string's `id`", redirecting an
|
|
18
|
+
* authorization lookup from a router-matched value to attacker-supplied input, caused by an
|
|
19
|
+
* unrelated route rename. `:` is decidable in one character, and no gate-param string written before
|
|
20
|
+
* this existed can contain one.
|
|
21
|
+
*/
|
|
22
|
+
export const RESOURCE_SOURCE_SEPARATOR = ':';
|
|
23
|
+
/** Separates path segments inside a qualified selector: `@body:order.id`. */
|
|
24
|
+
export const RESOURCE_PATH_SEPARATOR = '.';
|
|
25
|
+
/** Where a qualified selector may read a resource id from. */
|
|
26
|
+
export var GateParamSource;
|
|
27
|
+
(function (GateParamSource) {
|
|
28
|
+
GateParamSource["Params"] = "params";
|
|
29
|
+
GateParamSource["Query"] = "query";
|
|
30
|
+
GateParamSource["Body"] = "body";
|
|
31
|
+
GateParamSource["Headers"] = "headers";
|
|
32
|
+
GateParamSource["Auth"] = "auth";
|
|
33
|
+
})(GateParamSource || (GateParamSource = {}));
|
|
34
|
+
/**
|
|
35
|
+
* What the bare form (`@enquiryId`) searches, in order.
|
|
36
|
+
*
|
|
37
|
+
* Params first, then query. The query fallback is legacy and deliberately kept: removing it would
|
|
38
|
+
* silently deny already-deployed applications whose selector happens to name a query key, and a
|
|
39
|
+
* silent denial inside a customer's running app is the worst available outcome.
|
|
40
|
+
*/
|
|
41
|
+
export const DEFAULT_GATE_PARAM_SOURCES = [
|
|
42
|
+
GateParamSource.Params, GateParamSource.Query
|
|
43
|
+
];
|
|
44
|
+
/** Why a gate param is structurally wrong — a misconfiguration, not a failed authorization. */
|
|
45
|
+
export var GateParamErrorCode;
|
|
46
|
+
(function (GateParamErrorCode) {
|
|
47
|
+
GateParamErrorCode["UnknownSource"] = "gate-param:unknown-source";
|
|
48
|
+
GateParamErrorCode["EmptySelector"] = "gate-param:empty-selector";
|
|
49
|
+
GateParamErrorCode["EmptySegment"] = "gate-param:empty-segment";
|
|
50
|
+
/** Named a request key the entrypoint's filter does not declare — AJV strips it before the gate. */
|
|
51
|
+
GateParamErrorCode["UnreachableKey"] = "gate-param:unreachable-key";
|
|
52
|
+
/** Named a route param the entrypoint's own path does not carry. */
|
|
53
|
+
GateParamErrorCode["UnknownRouteParam"] = "gate-param:unknown-route-param";
|
|
54
|
+
})(GateParamErrorCode || (GateParamErrorCode = {}));
|
|
55
|
+
/** Why a selector produced no resource id. */
|
|
56
|
+
export var GateResolutionFailure;
|
|
57
|
+
(function (GateResolutionFailure) {
|
|
58
|
+
/** The source exists, the path does not. */
|
|
59
|
+
GateResolutionFailure["NotProvided"] = "not-provided";
|
|
60
|
+
/** The request carries no such source at all. */
|
|
61
|
+
GateResolutionFailure["SourceMissing"] = "source-missing";
|
|
62
|
+
/** Resolved to an object or a multi-element array — refused rather than stringified. */
|
|
63
|
+
GateResolutionFailure["NotScalar"] = "not-scalar";
|
|
64
|
+
/** The selector itself did not parse. */
|
|
65
|
+
GateResolutionFailure["Malformed"] = "malformed";
|
|
66
|
+
})(GateResolutionFailure || (GateResolutionFailure = {}));
|
|
67
|
+
/**
|
|
68
|
+
* Which FORM of a grant an operation addresses.
|
|
69
|
+
*
|
|
70
|
+
* A resource-scoped permission is grantable two ways, under one name: `Blanket` (a PermissionSet
|
|
71
|
+
* carrying no `resources`, which `hasPermission` reads as covering every id) and `Resources` (an
|
|
72
|
+
* explicit id list). They are stored separately and must be revoked separately, or a UI offering
|
|
73
|
+
* both controls would have one silently wipe the other.
|
|
74
|
+
*/
|
|
75
|
+
export var IamGrantMode;
|
|
76
|
+
(function (IamGrantMode) {
|
|
77
|
+
IamGrantMode["Blanket"] = "blanket";
|
|
78
|
+
IamGrantMode["Resources"] = "resources";
|
|
79
|
+
/** Revoke-only: every form. Illegal on a grant. */
|
|
80
|
+
IamGrantMode["All"] = "all";
|
|
81
|
+
})(IamGrantMode || (IamGrantMode = {}));
|
|
82
|
+
/** What to do about grants that still reference a definition being removed. */
|
|
83
|
+
export var IamRemovalPolicy;
|
|
84
|
+
(function (IamRemovalPolicy) {
|
|
85
|
+
/** Strip the permission from every subject of the entity, then drop the definition. */
|
|
86
|
+
IamRemovalPolicy["Cascade"] = "cascade";
|
|
87
|
+
/** Refuse while any subject still holds it; the holders travel in the error. */
|
|
88
|
+
IamRemovalPolicy["Refuse"] = "refuse";
|
|
89
|
+
})(IamRemovalPolicy || (IamRemovalPolicy = {}));
|
|
90
|
+
/**
|
|
91
|
+
* The grouping tiers a permission definition may be tagged with.
|
|
92
|
+
*
|
|
93
|
+
* A tag decides NOTHING at request time — `hasPermission` and every gate treat a permission name as
|
|
94
|
+
* one opaque string — and only groups the permission in an administrator's screen. `guest` is
|
|
95
|
+
* deliberately absent: a public surface declares no permissions, and adding an area later would
|
|
96
|
+
* re-classify names already tagged.
|
|
97
|
+
*/
|
|
98
|
+
export const IAM_AREAS = ['user', 'operator', 'admin'];
|
|
99
|
+
/** Separates the resource from the action in a permission name. TWO hyphens, never one. */
|
|
100
|
+
export const PERMISSION_ACTION_SEPARATOR = '--';
|
|
4
101
|
//# sourceMappingURL=consts.js.map
|
package/build/consts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAA;AAE1C,MAAM,CAAC,MAAM,iBAAiB,GAAG,UAAU,CAAA;AAC3C,MAAM,CAAC,MAAM,mBAAmB,GAAG,YAAY,CAAA"}
|
|
1
|
+
{"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAA;AAE1C,MAAM,CAAC,MAAM,iBAAiB,GAAG,UAAU,CAAA;AAC3C,MAAM,CAAC,MAAM,mBAAmB,GAAG,YAAY,CAAA;AAI/C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,CAAA;AAE3C;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,GAAG,CAAA;AAE5C,6EAA6E;AAC7E,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAA;AAE1C,8DAA8D;AAC9D,MAAM,CAAN,IAAY,eAMX;AAND,WAAY,eAAe;IACzB,oCAAiB,CAAA;IACjB,kCAAe,CAAA;IACf,gCAAa,CAAA;IACb,sCAAmB,CAAA;IACnB,gCAAa,CAAA;AACf,CAAC,EANW,eAAe,KAAf,eAAe,QAM1B;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAsB;IAC3D,eAAe,CAAC,MAAM,EAAE,eAAe,CAAC,KAAK;CAC9C,CAAA;AAED,+FAA+F;AAC/F,MAAM,CAAN,IAAY,kBAQX;AARD,WAAY,kBAAkB;IAC5B,iEAA2C,CAAA;IAC3C,iEAA2C,CAAA;IAC3C,+DAAyC,CAAA;IACzC,oGAAoG;IACpG,mEAA6C,CAAA;IAC7C,oEAAoE;IACpE,0EAAoD,CAAA;AACtD,CAAC,EARW,kBAAkB,KAAlB,kBAAkB,QAQ7B;AAED,8CAA8C;AAC9C,MAAM,CAAN,IAAY,qBASX;AATD,WAAY,qBAAqB;IAC/B,4CAA4C;IAC5C,qDAA4B,CAAA;IAC5B,iDAAiD;IACjD,yDAAgC,CAAA;IAChC,wFAAwF;IACxF,iDAAwB,CAAA;IACxB,yCAAyC;IACzC,gDAAuB,CAAA;AACzB,CAAC,EATW,qBAAqB,KAArB,qBAAqB,QAShC;AAED;;;;;;;GAOG;AACH,MAAM,CAAN,IAAY,YAKX;AALD,WAAY,YAAY;IACtB,mCAAmB,CAAA;IACnB,uCAAuB,CAAA;IACvB,mDAAmD;IACnD,2BAAW,CAAA;AACb,CAAC,EALW,YAAY,KAAZ,YAAY,QAKvB;AAED,+EAA+E;AAC/E,MAAM,CAAN,IAAY,gBAKX;AALD,WAAY,gBAAgB;IAC1B,uFAAuF;IACvF,uCAAmB,CAAA;IACnB,gFAAgF;IAChF,qCAAiB,CAAA;AACnB,CAAC,EALW,gBAAgB,KAAhB,gBAAgB,QAK3B;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,CAAU,CAAA;AAI/D,2FAA2F;AAC3F,MAAM,CAAC,MAAM,2BAA2B,GAAG,IAAI,CAAA"}
|
package/build/errors.d.ts
CHANGED
|
@@ -15,6 +15,19 @@ export declare class IamGrantError extends IamError {
|
|
|
15
15
|
static typeName: string;
|
|
16
16
|
constructor(message?: string);
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* A refusal about a permission DEFINITION, as opposed to a grant.
|
|
20
|
+
*
|
|
21
|
+
* Separate from `IamGrantError`, which means a grant's subject is missing or its entity does not
|
|
22
|
+
* match — reusing it for a definition-level refusal is a category error the next reader has to unpick.
|
|
23
|
+
*
|
|
24
|
+
* Codes: `permission:held:<name>` (still granted to someone, under a policy that refuses),
|
|
25
|
+
* `permission:managed:<name>` (platform-owned, deleted only with an explicit second key).
|
|
26
|
+
*/
|
|
27
|
+
export declare class IamPermissionError extends IamError {
|
|
28
|
+
static typeName: string;
|
|
29
|
+
constructor(message?: string);
|
|
30
|
+
}
|
|
18
31
|
export declare class IamUnsupported extends IamError {
|
|
19
32
|
static typeName: string;
|
|
20
33
|
constructor(message?: string);
|
package/build/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,qBAAa,QAAS,SAAQ,cAAc;IAC1C,OAAuB,QAAQ,SAAkC;IAEjE,YAAY,OAAO,SAAU,EAG5B;CACF;AAED,qBAAa,cAAe,SAAQ,QAAQ;IAC1C,OAAuB,QAAQ,SAAwC;IAEvE,YAAY,OAAO,SAAU,EAG5B;CACF;AAED,qBAAa,gBAAiB,SAAQ,QAAQ;IAC5C,OAAuB,QAAQ,SAA0C;IAEzE,YAAY,OAAO,SAAU,EAG5B;CACF;AAED,qBAAa,aAAc,SAAQ,QAAQ;IACzC,OAAuB,QAAQ,SAAuC;IAEtE,YAAY,OAAO,SAAU,EAG5B;CACF;AAED,qBAAa,cAAe,SAAQ,QAAQ;IAC1C,OAAuB,QAAQ,SAA6C;IAE5E,YAAY,OAAO,SAAU,EAG5B;CACF;AAED,qBAAa,YAAa,SAAQ,QAAQ;IACxC,OAAuB,QAAQ,SAAsC;IAErE,YAAY,OAAO,SAAU,EAG5B;CACF"}
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,qBAAa,QAAS,SAAQ,cAAc;IAC1C,OAAuB,QAAQ,SAAkC;IAEjE,YAAY,OAAO,SAAU,EAG5B;CACF;AAED,qBAAa,cAAe,SAAQ,QAAQ;IAC1C,OAAuB,QAAQ,SAAwC;IAEvE,YAAY,OAAO,SAAU,EAG5B;CACF;AAED,qBAAa,gBAAiB,SAAQ,QAAQ;IAC5C,OAAuB,QAAQ,SAA0C;IAEzE,YAAY,OAAO,SAAU,EAG5B;CACF;AAED,qBAAa,aAAc,SAAQ,QAAQ;IACzC,OAAuB,QAAQ,SAAuC;IAEtE,YAAY,OAAO,SAAU,EAG5B;CACF;AAED;;;;;;;;GAQG;AACH,qBAAa,kBAAmB,SAAQ,QAAQ;IAC9C,OAAuB,QAAQ,SAA4C;IAE3E,YAAY,OAAO,SAAU,EAG5B;CACF;AAED,qBAAa,cAAe,SAAQ,QAAQ;IAC1C,OAAuB,QAAQ,SAA6C;IAE5E,YAAY,OAAO,SAAU,EAG5B;CACF;AAED,qBAAa,YAAa,SAAQ,QAAQ;IACxC,OAAuB,QAAQ,SAAsC;IAErE,YAAY,OAAO,SAAU,EAG5B;CACF"}
|
package/build/errors.js
CHANGED
|
@@ -27,6 +27,22 @@ export class IamGrantError extends IamError {
|
|
|
27
27
|
this.type = IamGrantError.typeName;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* A refusal about a permission DEFINITION, as opposed to a grant.
|
|
32
|
+
*
|
|
33
|
+
* Separate from `IamGrantError`, which means a grant's subject is missing or its entity does not
|
|
34
|
+
* match — reusing it for a definition-level refusal is a category error the next reader has to unpick.
|
|
35
|
+
*
|
|
36
|
+
* Codes: `permission:held:<name>` (still granted to someone, under a policy that refuses),
|
|
37
|
+
* `permission:managed:<name>` (platform-owned, deleted only with an explicit second key).
|
|
38
|
+
*/
|
|
39
|
+
export class IamPermissionError extends IamError {
|
|
40
|
+
static typeName = `IamPermission${ResilientError.typeName}`;
|
|
41
|
+
constructor(message = 'error') {
|
|
42
|
+
super(`permission:${message}`);
|
|
43
|
+
this.type = IamPermissionError.typeName;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
30
46
|
export class IamUnsupported extends IamError {
|
|
31
47
|
static typeName = `IamUnsupported${ResilientError.typeName}`;
|
|
32
48
|
constructor(message = 'error') {
|
|
@@ -45,6 +61,7 @@ ResilientError.registerErrorClass(IamError);
|
|
|
45
61
|
ResilientError.registerErrorClass(IamClientError);
|
|
46
62
|
ResilientError.registerErrorClass(IamResourceError);
|
|
47
63
|
ResilientError.registerErrorClass(IamGrantError);
|
|
64
|
+
ResilientError.registerErrorClass(IamPermissionError);
|
|
48
65
|
ResilientError.registerErrorClass(IamUnsupported);
|
|
49
66
|
ResilientError.registerErrorClass(IamUserError);
|
|
50
67
|
//# sourceMappingURL=errors.js.map
|
package/build/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,MAAM,OAAO,QAAS,SAAQ,cAAc;IACnC,MAAM,CAAU,QAAQ,GAAG,MAAM,cAAc,CAAC,QAAQ,EAAE,CAAA;IAEjE,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,OAAO,EAAE,CAAC,CAAA;QAC1C,IAAI,CAAC,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAA;IAC/B,CAAC;CACF;AAED,MAAM,OAAO,cAAe,SAAQ,QAAQ;IACnC,MAAM,CAAU,QAAQ,GAAG,YAAY,cAAc,CAAC,QAAQ,EAAE,CAAA;IAEvE,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,UAAU,OAAO,EAAE,CAAC,CAAA;QAC1B,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC,QAAQ,CAAA;IACrC,CAAC;CACF;AAED,MAAM,OAAO,gBAAiB,SAAQ,QAAQ;IACrC,MAAM,CAAU,QAAQ,GAAG,cAAc,cAAc,CAAC,QAAQ,EAAE,CAAA;IAEzE,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,YAAY,OAAO,EAAE,CAAC,CAAA;QAC5B,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC,QAAQ,CAAA;IACvC,CAAC;CACF;AAED,MAAM,OAAO,aAAc,SAAQ,QAAQ;IAClC,MAAM,CAAU,QAAQ,GAAG,WAAW,cAAc,CAAC,QAAQ,EAAE,CAAA;IAEtE,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,SAAS,OAAO,EAAE,CAAC,CAAA;QACzB,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC,QAAQ,CAAA;IACpC,CAAC;CACF;AAED,MAAM,OAAO,cAAe,SAAQ,QAAQ;IACnC,MAAM,CAAU,QAAQ,GAAG,iBAAiB,cAAc,CAAC,QAAQ,EAAE,CAAA;IAE5E,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,eAAe,OAAO,EAAE,CAAC,CAAA;QAC/B,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC,QAAQ,CAAA;IACrC,CAAC;CACF;AAED,MAAM,OAAO,YAAa,SAAQ,QAAQ;IACjC,MAAM,CAAU,QAAQ,GAAG,UAAU,cAAc,CAAC,QAAQ,EAAE,CAAA;IAErE,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,QAAQ,OAAO,EAAE,CAAC,CAAA;QACxB,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC,QAAQ,CAAA;IACnC,CAAC;CACF;AAED,cAAc,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAA;AAC3C,cAAc,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAA;AACjD,cAAc,CAAC,kBAAkB,CAAC,gBAAgB,CAAC,CAAA;AACnD,cAAc,CAAC,kBAAkB,CAAC,aAAa,CAAC,CAAA;AAChD,cAAc,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAA;AACjD,cAAc,CAAC,kBAAkB,CAAC,YAAY,CAAC,CAAA"}
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,MAAM,OAAO,QAAS,SAAQ,cAAc;IACnC,MAAM,CAAU,QAAQ,GAAG,MAAM,cAAc,CAAC,QAAQ,EAAE,CAAA;IAEjE,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,OAAO,EAAE,CAAC,CAAA;QAC1C,IAAI,CAAC,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAA;IAC/B,CAAC;CACF;AAED,MAAM,OAAO,cAAe,SAAQ,QAAQ;IACnC,MAAM,CAAU,QAAQ,GAAG,YAAY,cAAc,CAAC,QAAQ,EAAE,CAAA;IAEvE,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,UAAU,OAAO,EAAE,CAAC,CAAA;QAC1B,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC,QAAQ,CAAA;IACrC,CAAC;CACF;AAED,MAAM,OAAO,gBAAiB,SAAQ,QAAQ;IACrC,MAAM,CAAU,QAAQ,GAAG,cAAc,cAAc,CAAC,QAAQ,EAAE,CAAA;IAEzE,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,YAAY,OAAO,EAAE,CAAC,CAAA;QAC5B,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC,QAAQ,CAAA;IACvC,CAAC;CACF;AAED,MAAM,OAAO,aAAc,SAAQ,QAAQ;IAClC,MAAM,CAAU,QAAQ,GAAG,WAAW,cAAc,CAAC,QAAQ,EAAE,CAAA;IAEtE,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,SAAS,OAAO,EAAE,CAAC,CAAA;QACzB,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC,QAAQ,CAAA;IACpC,CAAC;CACF;AAED;;;;;;;;GAQG;AACH,MAAM,OAAO,kBAAmB,SAAQ,QAAQ;IACvC,MAAM,CAAU,QAAQ,GAAG,gBAAgB,cAAc,CAAC,QAAQ,EAAE,CAAA;IAE3E,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,cAAc,OAAO,EAAE,CAAC,CAAA;QAC9B,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC,QAAQ,CAAA;IACzC,CAAC;CACF;AAED,MAAM,OAAO,cAAe,SAAQ,QAAQ;IACnC,MAAM,CAAU,QAAQ,GAAG,iBAAiB,cAAc,CAAC,QAAQ,EAAE,CAAA;IAE5E,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,eAAe,OAAO,EAAE,CAAC,CAAA;QAC/B,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC,QAAQ,CAAA;IACrC,CAAC;CACF;AAED,MAAM,OAAO,YAAa,SAAQ,QAAQ;IACjC,MAAM,CAAU,QAAQ,GAAG,UAAU,cAAc,CAAC,QAAQ,EAAE,CAAA;IAErE,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,QAAQ,OAAO,EAAE,CAAC,CAAA;QACxB,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC,QAAQ,CAAA;IACnC,CAAC;CACF;AAED,cAAc,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAA;AAC3C,cAAc,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAA;AACjD,cAAc,CAAC,kBAAkB,CAAC,gBAAgB,CAAC,CAAA;AACnD,cAAc,CAAC,kBAAkB,CAAC,aAAa,CAAC,CAAA;AAChD,cAAc,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,CAAA;AACrD,cAAc,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAA;AACjD,cAAc,CAAC,kBAAkB,CAAC,YAAY,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/gate/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,eAAe,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/gate/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,eAAe,CAAA"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { GateParamSource } from '../consts.js';
|
|
2
|
+
import type { GateResourceSelector, GateParamProblem, ParsedGateParam } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Read the `@…` half of a gate param.
|
|
5
|
+
*
|
|
6
|
+
* Two forms, and the difference between them is deliberate:
|
|
7
|
+
*
|
|
8
|
+
* `@enquiryId` — bare. A FLAT key, searched in `params` then `query`. It is never split on
|
|
9
|
+
* a dot, because a query key legally contains one and splitting it would
|
|
10
|
+
* change the meaning of selectors already deployed.
|
|
11
|
+
* `@body:order.id` — qualified. An explicit source and a nested path.
|
|
12
|
+
*
|
|
13
|
+
* The rule worth remembering: the bare form is lenient and flat, the qualified form is strict and
|
|
14
|
+
* nested.
|
|
15
|
+
*/
|
|
16
|
+
export declare const parseGateSelector: (selector: string) => GateResourceSelector | GateParamProblem;
|
|
17
|
+
/**
|
|
18
|
+
* Split a gate param into the permission it checks and where the resource id comes from.
|
|
19
|
+
*
|
|
20
|
+
* Splits at the FIRST `@`. Everything before it is the permission name looked up in the subject's
|
|
21
|
+
* grants; everything after says where to read the resource id at request time. The suffix is the
|
|
22
|
+
* gate's syntax and nothing else's — a permission stored or granted under a name containing one is a
|
|
23
|
+
* key nothing ever looks up.
|
|
24
|
+
*/
|
|
25
|
+
export declare const parseGateParam: (param: string) => ParsedGateParam;
|
|
26
|
+
/** Compose a gate param. The inverse of `parseGateParam` for every selector it accepts. */
|
|
27
|
+
export declare const formatGateParam: (permission: string, selector?: string | {
|
|
28
|
+
source?: GateParamSource;
|
|
29
|
+
path: string[];
|
|
30
|
+
}) => string;
|
|
31
|
+
//# sourceMappingURL=param.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"param.d.ts","sourceRoot":"","sources":["../../src/gate/param.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,eAAe,EAChB,MAAM,cAAc,CAAA;AACrB,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAI1F;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iBAAiB,aAAc,MAAM,KAAG,oBAAoB,GAAG,gBA6C3E,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,UAAW,MAAM,KAAG,eAoB9C,CAAA;AAED,2FAA2F;AAC3F,eAAO,MAAM,eAAe,eACd,MAAM,aACP,MAAM,GAAG;IAAE,MAAM,CAAC,EAAE,eAAe,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,KAC/D,MAmBF,CAAA"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { RESOURCE_PARAM_SEPARATOR, RESOURCE_SOURCE_SEPARATOR, RESOURCE_PATH_SEPARATOR, GateParamSource, GateParamErrorCode, DEFAULT_GATE_PARAM_SOURCES } from '../consts.js';
|
|
2
|
+
const SOURCES = Object.values(GateParamSource);
|
|
3
|
+
/**
|
|
4
|
+
* Read the `@…` half of a gate param.
|
|
5
|
+
*
|
|
6
|
+
* Two forms, and the difference between them is deliberate:
|
|
7
|
+
*
|
|
8
|
+
* `@enquiryId` — bare. A FLAT key, searched in `params` then `query`. It is never split on
|
|
9
|
+
* a dot, because a query key legally contains one and splitting it would
|
|
10
|
+
* change the meaning of selectors already deployed.
|
|
11
|
+
* `@body:order.id` — qualified. An explicit source and a nested path.
|
|
12
|
+
*
|
|
13
|
+
* The rule worth remembering: the bare form is lenient and flat, the qualified form is strict and
|
|
14
|
+
* nested.
|
|
15
|
+
*/
|
|
16
|
+
export const parseGateSelector = (selector) => {
|
|
17
|
+
if (selector === '') {
|
|
18
|
+
return { code: GateParamErrorCode.EmptySelector, detail: 'the selector after "@" is empty' };
|
|
19
|
+
}
|
|
20
|
+
const idx = selector.indexOf(RESOURCE_SOURCE_SEPARATOR);
|
|
21
|
+
if (idx < 0) {
|
|
22
|
+
return {
|
|
23
|
+
selector,
|
|
24
|
+
path: [selector],
|
|
25
|
+
sources: DEFAULT_GATE_PARAM_SOURCES
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const source = selector.slice(0, idx);
|
|
29
|
+
const rest = selector.slice(idx + 1);
|
|
30
|
+
if (!SOURCES.includes(source)) {
|
|
31
|
+
return {
|
|
32
|
+
code: GateParamErrorCode.UnknownSource,
|
|
33
|
+
detail: `"${source}" is not one of ${SOURCES.join(', ')}`
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (rest === '') {
|
|
37
|
+
return {
|
|
38
|
+
code: GateParamErrorCode.EmptySelector,
|
|
39
|
+
detail: `"${source}:" names no path`
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const path = rest.split(RESOURCE_PATH_SEPARATOR);
|
|
43
|
+
if (path.some(segment => segment === '')) {
|
|
44
|
+
return {
|
|
45
|
+
code: GateParamErrorCode.EmptySegment,
|
|
46
|
+
detail: `"${rest}" has an empty path segment`
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
selector,
|
|
51
|
+
source: source,
|
|
52
|
+
path,
|
|
53
|
+
sources: [source]
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Split a gate param into the permission it checks and where the resource id comes from.
|
|
58
|
+
*
|
|
59
|
+
* Splits at the FIRST `@`. Everything before it is the permission name looked up in the subject's
|
|
60
|
+
* grants; everything after says where to read the resource id at request time. The suffix is the
|
|
61
|
+
* gate's syntax and nothing else's — a permission stored or granted under a name containing one is a
|
|
62
|
+
* key nothing ever looks up.
|
|
63
|
+
*/
|
|
64
|
+
export const parseGateParam = (param) => {
|
|
65
|
+
const idx = param.indexOf(RESOURCE_PARAM_SEPARATOR);
|
|
66
|
+
if (idx < 0) {
|
|
67
|
+
return { permission: param };
|
|
68
|
+
}
|
|
69
|
+
const permission = param.slice(0, idx);
|
|
70
|
+
const selector = param.slice(idx + 1);
|
|
71
|
+
const parsed = parseGateSelector(selector);
|
|
72
|
+
if ('code' in parsed) {
|
|
73
|
+
return { permission, resourceParam: selector, error: parsed };
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
permission,
|
|
77
|
+
// Kept for readers written against the flat form. Only meaningful when the form IS flat.
|
|
78
|
+
...(parsed.source == null ? { resourceParam: selector } : {}),
|
|
79
|
+
resource: parsed
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
/** Compose a gate param. The inverse of `parseGateParam` for every selector it accepts. */
|
|
83
|
+
export const formatGateParam = (permission, selector) => {
|
|
84
|
+
if (selector == null) {
|
|
85
|
+
return permission;
|
|
86
|
+
}
|
|
87
|
+
if (typeof selector === 'string') {
|
|
88
|
+
return selector === '' ? permission : `${permission}${RESOURCE_PARAM_SEPARATOR}${selector}`;
|
|
89
|
+
}
|
|
90
|
+
if (selector.path.length < 1) {
|
|
91
|
+
return permission;
|
|
92
|
+
}
|
|
93
|
+
const path = selector.path.join(RESOURCE_PATH_SEPARATOR);
|
|
94
|
+
const tail = selector.source == null
|
|
95
|
+
? path
|
|
96
|
+
: `${selector.source}${RESOURCE_SOURCE_SEPARATOR}${path}`;
|
|
97
|
+
return `${permission}${RESOURCE_PARAM_SEPARATOR}${tail}`;
|
|
98
|
+
};
|
|
99
|
+
//# sourceMappingURL=param.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"param.js","sourceRoot":"","sources":["../../src/gate/param.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { GateRequestLike, GateResourceSelector, GateResourceResolution } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the resource id a selector points at.
|
|
4
|
+
*
|
|
5
|
+
* Sources are tried in the selector's own order — for the bare form, route params before the query —
|
|
6
|
+
* and the first that yields a value wins. When none does, the reason travels back so the caller can
|
|
7
|
+
* tell a MISCONFIGURED gate (the request never carried that key) from a legitimately DENIED one.
|
|
8
|
+
*/
|
|
9
|
+
export declare const resolveGateResource: (req: GateRequestLike, selector: GateResourceSelector) => GateResourceResolution;
|
|
10
|
+
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/gate/resolve.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AA4EhG;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,QACzB,eAAe,YAAY,oBAAoB,KACnD,sBA2BF,CAAA"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { GateParamSource, GateResolutionFailure } from '../consts.js';
|
|
2
|
+
const isRecord = (value) => value != null && typeof value === 'object';
|
|
3
|
+
/**
|
|
4
|
+
* A resource id is a scalar. Anything else is refused rather than coerced.
|
|
5
|
+
*
|
|
6
|
+
* Stringifying an object would produce `"[object Object]"`, which then either matches nothing or —
|
|
7
|
+
* far worse — matches another record that stringified the same way.
|
|
8
|
+
*/
|
|
9
|
+
const scalar = (value) => {
|
|
10
|
+
switch (typeof value) {
|
|
11
|
+
case 'string':
|
|
12
|
+
return value;
|
|
13
|
+
case 'number':
|
|
14
|
+
case 'boolean':
|
|
15
|
+
case 'bigint':
|
|
16
|
+
return `${value}`;
|
|
17
|
+
default:
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const sourceOf = (req, source) => {
|
|
22
|
+
switch (source) {
|
|
23
|
+
case GateParamSource.Params: return req.params;
|
|
24
|
+
case GateParamSource.Query: return req.query;
|
|
25
|
+
case GateParamSource.Body: return req.body;
|
|
26
|
+
case GateParamSource.Headers: return req.headers;
|
|
27
|
+
case GateParamSource.Auth: return req.auth;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Walk one source for the selector's path.
|
|
32
|
+
*
|
|
33
|
+
* Headers get two adjustments they need and nothing else does: the first segment is lowercased,
|
|
34
|
+
* because HTTP header names are case-insensitive and every server normalises them differently; and a
|
|
35
|
+
* single-element array unwraps, because a repeated header arrives as an array and silently picking
|
|
36
|
+
* one of several values would be an authorization decision made by accident.
|
|
37
|
+
*/
|
|
38
|
+
const walk = (container, path, source) => {
|
|
39
|
+
if (!isRecord(container)) {
|
|
40
|
+
return { reason: GateResolutionFailure.SourceMissing };
|
|
41
|
+
}
|
|
42
|
+
let cursor = container;
|
|
43
|
+
for (const [index, rawSegment] of path.entries()) {
|
|
44
|
+
if (!isRecord(cursor)) {
|
|
45
|
+
return { reason: GateResolutionFailure.NotProvided };
|
|
46
|
+
}
|
|
47
|
+
const segment = source === GateParamSource.Headers && index === 0
|
|
48
|
+
? rawSegment.toLowerCase()
|
|
49
|
+
: rawSegment;
|
|
50
|
+
cursor = cursor[segment];
|
|
51
|
+
}
|
|
52
|
+
if (source === GateParamSource.Headers && Array.isArray(cursor)) {
|
|
53
|
+
if (cursor.length !== 1) {
|
|
54
|
+
return { reason: GateResolutionFailure.NotScalar };
|
|
55
|
+
}
|
|
56
|
+
cursor = cursor[0];
|
|
57
|
+
}
|
|
58
|
+
if (cursor == null) {
|
|
59
|
+
return { reason: GateResolutionFailure.NotProvided };
|
|
60
|
+
}
|
|
61
|
+
return { value: cursor };
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the resource id a selector points at.
|
|
65
|
+
*
|
|
66
|
+
* Sources are tried in the selector's own order — for the bare form, route params before the query —
|
|
67
|
+
* and the first that yields a value wins. When none does, the reason travels back so the caller can
|
|
68
|
+
* tell a MISCONFIGURED gate (the request never carried that key) from a legitimately DENIED one.
|
|
69
|
+
*/
|
|
70
|
+
export const resolveGateResource = (req, selector) => {
|
|
71
|
+
let reason = GateResolutionFailure.SourceMissing;
|
|
72
|
+
for (const source of selector.sources) {
|
|
73
|
+
const result = walk(sourceOf(req, source), selector.path, source);
|
|
74
|
+
if (result.reason != null) {
|
|
75
|
+
// A hard refusal is more informative than "keep looking" — report it rather than letting a
|
|
76
|
+
// later, emptier source overwrite it with a vaguer reason.
|
|
77
|
+
if (result.reason === GateResolutionFailure.NotScalar) {
|
|
78
|
+
return { reason: result.reason, from: source };
|
|
79
|
+
}
|
|
80
|
+
if (reason === GateResolutionFailure.SourceMissing) {
|
|
81
|
+
reason = result.reason;
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const value = scalar(result.value);
|
|
86
|
+
if (value == null) {
|
|
87
|
+
return { reason: GateResolutionFailure.NotScalar, from: source };
|
|
88
|
+
}
|
|
89
|
+
return { id: value, from: source };
|
|
90
|
+
}
|
|
91
|
+
return { reason };
|
|
92
|
+
};
|
|
93
|
+
//# sourceMappingURL=resolve.js.map
|