@plantops/iam-client 0.1.0
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/README.md +11 -0
- package/dist/auth.d.ts +135 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +168 -0
- package/dist/client.d.ts +110 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +135 -0
- package/dist/endpoints/applications.d.ts +48 -0
- package/dist/endpoints/applications.d.ts.map +1 -0
- package/dist/endpoints/applications.js +30 -0
- package/dist/endpoints/audit.d.ts +43 -0
- package/dist/endpoints/audit.d.ts.map +1 -0
- package/dist/endpoints/audit.js +41 -0
- package/dist/endpoints/auth.d.ts +56 -0
- package/dist/endpoints/auth.d.ts.map +1 -0
- package/dist/endpoints/auth.js +88 -0
- package/dist/endpoints/authz.d.ts +31 -0
- package/dist/endpoints/authz.d.ts.map +1 -0
- package/dist/endpoints/authz.js +39 -0
- package/dist/endpoints/bindings.d.ts +17 -0
- package/dist/endpoints/bindings.d.ts.map +1 -0
- package/dist/endpoints/bindings.js +16 -0
- package/dist/endpoints/clients.d.ts +35 -0
- package/dist/endpoints/clients.d.ts.map +1 -0
- package/dist/endpoints/clients.js +31 -0
- package/dist/endpoints/entitlements.d.ts +40 -0
- package/dist/endpoints/entitlements.d.ts.map +1 -0
- package/dist/endpoints/entitlements.js +43 -0
- package/dist/endpoints/index.d.ts +22 -0
- package/dist/endpoints/index.d.ts.map +1 -0
- package/dist/endpoints/index.js +21 -0
- package/dist/endpoints/navigation.d.ts +25 -0
- package/dist/endpoints/navigation.d.ts.map +1 -0
- package/dist/endpoints/navigation.js +24 -0
- package/dist/endpoints/roles.d.ts +25 -0
- package/dist/endpoints/roles.d.ts.map +1 -0
- package/dist/endpoints/roles.js +21 -0
- package/dist/endpoints/scopes.d.ts +19 -0
- package/dist/endpoints/scopes.d.ts.map +1 -0
- package/dist/endpoints/scopes.js +18 -0
- package/dist/endpoints/service-accounts.d.ts +19 -0
- package/dist/endpoints/service-accounts.d.ts.map +1 -0
- package/dist/endpoints/service-accounts.js +19 -0
- package/dist/endpoints/users.d.ts +39 -0
- package/dist/endpoints/users.d.ts.map +1 -0
- package/dist/endpoints/users.js +24 -0
- package/dist/errors.d.ts +70 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +113 -0
- package/dist/http.d.ts +113 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +164 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/lib/iam-client.d.ts +2 -0
- package/dist/lib/iam-client.d.ts.map +1 -0
- package/dist/lib/iam-client.js +3 -0
- package/dist/resolve-cache.d.ts +65 -0
- package/dist/resolve-cache.d.ts.map +1 -0
- package/dist/resolve-cache.js +98 -0
- package/dist/testing/mock-server.d.ts +65 -0
- package/dist/testing/mock-server.d.ts.map +1 -0
- package/dist/testing/mock-server.js +107 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/package.json +40 -0
package/README.md
ADDED
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The token lifecycle: where credentials are kept, and when they are renewed.
|
|
3
|
+
*
|
|
4
|
+
* Separated from the `/auth/*` endpoint methods in `endpoints/auth.ts` on
|
|
5
|
+
* purpose. Those are HTTP; this is state — and it is the state two very
|
|
6
|
+
* different consumers need to control. A future operational module holds one
|
|
7
|
+
* service-account token in memory for the process's lifetime; `admin-web` holds
|
|
8
|
+
* a human's pair somewhere that survives a page reload, and would rather that
|
|
9
|
+
* somewhere were its own decision than this library's (Doc 09 §1). So the store
|
|
10
|
+
* is a two-method port, {@link MemoryTokenStore} is the default, and nothing
|
|
11
|
+
* here ever names `localStorage`, a cookie, or a file.
|
|
12
|
+
*
|
|
13
|
+
* ## Single-flight refresh
|
|
14
|
+
*
|
|
15
|
+
* The property that matters, and the reason this is a class rather than a
|
|
16
|
+
* closure over a variable. A screen that loads six panels issues six requests
|
|
17
|
+
* with the same expired access token and gets six `401`s within a few
|
|
18
|
+
* milliseconds of each other. Six refreshes would follow — and because
|
|
19
|
+
* `POST /auth/refresh` **rotates** (Doc 03 §4), five of them would present a
|
|
20
|
+
* token the first has already consumed, which the server is right to treat as
|
|
21
|
+
* replay. One in-flight refresh is therefore not an optimisation but a
|
|
22
|
+
* correctness requirement: {@link TokenSession.refresh} hands every concurrent
|
|
23
|
+
* caller the same promise, and the reuse-detection grace window of Doc 03 §4.1
|
|
24
|
+
* covers only what this cannot.
|
|
25
|
+
*/
|
|
26
|
+
import type { AccessTokenResponse, TokenPairResponse } from '@plantops/contracts';
|
|
27
|
+
/** What a store holds. Access token, its renewal, and when it lapses. */
|
|
28
|
+
export interface StoredTokens {
|
|
29
|
+
accessToken: string;
|
|
30
|
+
/** `null` for a service account: `POST /auth/token` issues no refresh token. */
|
|
31
|
+
refreshToken: string | null;
|
|
32
|
+
/**
|
|
33
|
+
* Epoch milliseconds, derived from `expires_in` when the pair was issued, or
|
|
34
|
+
* `null` when the tokens were adopted from elsewhere without one. `null`
|
|
35
|
+
* disables proactive renewal — the token is then used until a `401` says
|
|
36
|
+
* otherwise, which is correct, just one round trip slower.
|
|
37
|
+
*/
|
|
38
|
+
expiresAt: number | null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Where tokens live between calls.
|
|
42
|
+
*
|
|
43
|
+
* Both methods may be async so that a store can be a keychain, an encrypted
|
|
44
|
+
* file, or an `IndexedDB` handle. `write(null)` clears.
|
|
45
|
+
*/
|
|
46
|
+
export interface TokenStore {
|
|
47
|
+
read(): StoredTokens | null | Promise<StoredTokens | null>;
|
|
48
|
+
write(tokens: StoredTokens | null): void | Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
/** The default: tokens last as long as the client object does. */
|
|
51
|
+
export declare class MemoryTokenStore implements TokenStore {
|
|
52
|
+
private tokens;
|
|
53
|
+
read(): StoredTokens | null;
|
|
54
|
+
write(tokens: StoredTokens | null): void;
|
|
55
|
+
}
|
|
56
|
+
/** Why {@link TokenSessionOptions.onSessionEnded} fired. */
|
|
57
|
+
export type SessionEndReason = 'logout' | 'refresh_failed';
|
|
58
|
+
export interface TokenSessionOptions {
|
|
59
|
+
store?: TokenStore;
|
|
60
|
+
/** Injectable clock, so the expiry tests need no timers. */
|
|
61
|
+
now?: () => number;
|
|
62
|
+
/**
|
|
63
|
+
* Renew this many seconds before the access token actually lapses.
|
|
64
|
+
*
|
|
65
|
+
* Zero would mean every renewal costs a wasted round trip — the request that
|
|
66
|
+
* discovers the `401`. Thirty seconds is comfortably more than a slow request
|
|
67
|
+
* plus the 60-second clock skew the server already tolerates
|
|
68
|
+
* (`CLOCK_SKEW_LEEWAY_SECONDS`), and comfortably less than the 900-second
|
|
69
|
+
* access-token lifetime, so it neither renews constantly nor cuts it fine.
|
|
70
|
+
*/
|
|
71
|
+
refreshLeewaySeconds?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Fired when the session is over: the caller logged out, or a refresh failed
|
|
74
|
+
* and the stored tokens were dropped. `admin-web` sends the user to the login
|
|
75
|
+
* screen from here (Doc 09 §1).
|
|
76
|
+
*/
|
|
77
|
+
onSessionEnded?: (reason: SessionEndReason) => void;
|
|
78
|
+
}
|
|
79
|
+
export declare class TokenSession {
|
|
80
|
+
/** `POST /auth/refresh`, injected because it goes back through the transport. */
|
|
81
|
+
private readonly exchangeRefreshToken;
|
|
82
|
+
private readonly store;
|
|
83
|
+
private readonly now;
|
|
84
|
+
private readonly leewayMs;
|
|
85
|
+
private readonly onSessionEnded;
|
|
86
|
+
/** The one in-flight refresh every concurrent caller shares. */
|
|
87
|
+
private pending;
|
|
88
|
+
constructor(
|
|
89
|
+
/** `POST /auth/refresh`, injected because it goes back through the transport. */
|
|
90
|
+
exchangeRefreshToken: (refreshToken: string) => Promise<TokenPairResponse>, options?: TokenSessionOptions);
|
|
91
|
+
/** What is stored right now, without renewing anything. */
|
|
92
|
+
tokens(): Promise<StoredTokens | null>;
|
|
93
|
+
isAuthenticated(): Promise<boolean>;
|
|
94
|
+
/**
|
|
95
|
+
* The token to send, renewed first if it is about to lapse.
|
|
96
|
+
*
|
|
97
|
+
* A failed proactive renewal answers `null` rather than the token it could
|
|
98
|
+
* not replace: the stored pair is gone by then, and sending a token this
|
|
99
|
+
* object no longer holds would produce a `401` whose retry has nothing left
|
|
100
|
+
* to refresh with.
|
|
101
|
+
*/
|
|
102
|
+
accessToken(): Promise<string | null>;
|
|
103
|
+
/**
|
|
104
|
+
* The transport's `401` hook.
|
|
105
|
+
*
|
|
106
|
+
* Two things happen here that a plain "refresh on 401" would get wrong. The
|
|
107
|
+
* first is the check that the stored token is still the one that failed: when
|
|
108
|
+
* six requests race, five of them arrive after the refresh has already landed
|
|
109
|
+
* and simply need retrying with what is now stored — no second refresh, and
|
|
110
|
+
* no rotation of a token nobody has used yet. The second is that failure is
|
|
111
|
+
* `false`, not an exception: the caller's original `401` is the honest answer
|
|
112
|
+
* to their request, and replacing it with a refresh error would report the
|
|
113
|
+
* wrong failed call.
|
|
114
|
+
*/
|
|
115
|
+
reauthorize(usedToken: string | null): Promise<boolean>;
|
|
116
|
+
/**
|
|
117
|
+
* Renews the pair, sharing one exchange with every concurrent caller.
|
|
118
|
+
*
|
|
119
|
+
* A failed refresh clears the store: the refresh token is either expired,
|
|
120
|
+
* revoked, or has been replayed, and all three mean this session is over
|
|
121
|
+
* (Doc 03 §4.1). Keeping it would guarantee that every later call spends a
|
|
122
|
+
* round trip rediscovering the same thing.
|
|
123
|
+
*/
|
|
124
|
+
refresh(): Promise<TokenPairResponse>;
|
|
125
|
+
private runRefresh;
|
|
126
|
+
/** Takes the tokens a login, refresh or service-token exchange returned. */
|
|
127
|
+
adopt(tokens: TokenPairResponse | AccessTokenResponse): Promise<void>;
|
|
128
|
+
/** Adopts tokens from somewhere else — a server-rendered page, another tab. */
|
|
129
|
+
restore(tokens: StoredTokens | null): Promise<void>;
|
|
130
|
+
/** Drops the tokens locally. The server-side revocation is `POST /auth/logout`. */
|
|
131
|
+
clear(): Promise<void>;
|
|
132
|
+
private forget;
|
|
133
|
+
private expiringSoon;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAElF,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;;;;OAKG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,IAAI,YAAY,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAC3D,KAAK,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1D;AAED,kEAAkE;AAClE,qBAAa,gBAAiB,YAAW,UAAU;IACjD,OAAO,CAAC,MAAM,CAA6B;IAE3C,IAAI,IAAI,YAAY,GAAG,IAAI;IAI3B,KAAK,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,GAAG,IAAI;CAGzC;AAED,4DAA4D;AAC5D,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,gBAAgB,CAAC;AAE3D,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,4DAA4D;IAC5D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;CACrD;AAID,qBAAa,YAAY;IAYrB,iFAAiF;IACjF,OAAO,CAAC,QAAQ,CAAC,oBAAoB;IAZvC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAEjB;IAEd,gEAAgE;IAChE,OAAO,CAAC,OAAO,CAA2C;;IAGxD,iFAAiF;IAChE,oBAAoB,EAAE,CACrC,YAAY,EAAE,MAAM,KACjB,OAAO,CAAC,iBAAiB,CAAC,EAC/B,OAAO,GAAE,mBAAwB;IASnC,2DAA2D;IACrD,MAAM,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAItC,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC;IAIzC;;;;;;;OAOG;IACG,WAAW,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAe3C;;;;;;;;;;;OAWG;IACG,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC;IAc7D;;;;;;;OAOG;IACG,OAAO,IAAI,OAAO,CAAC,iBAAiB,CAAC;YAS7B,UAAU;IAgBxB,4EAA4E;IACtE,KAAK,CAAC,MAAM,EAAE,iBAAiB,GAAG,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAW3E,+EAA+E;IACzE,OAAO,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAIzD,mFAAmF;IAC7E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAId,MAAM;IAKpB,OAAO,CAAC,YAAY;CAGrB"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The token lifecycle: where credentials are kept, and when they are renewed.
|
|
3
|
+
*
|
|
4
|
+
* Separated from the `/auth/*` endpoint methods in `endpoints/auth.ts` on
|
|
5
|
+
* purpose. Those are HTTP; this is state — and it is the state two very
|
|
6
|
+
* different consumers need to control. A future operational module holds one
|
|
7
|
+
* service-account token in memory for the process's lifetime; `admin-web` holds
|
|
8
|
+
* a human's pair somewhere that survives a page reload, and would rather that
|
|
9
|
+
* somewhere were its own decision than this library's (Doc 09 §1). So the store
|
|
10
|
+
* is a two-method port, {@link MemoryTokenStore} is the default, and nothing
|
|
11
|
+
* here ever names `localStorage`, a cookie, or a file.
|
|
12
|
+
*
|
|
13
|
+
* ## Single-flight refresh
|
|
14
|
+
*
|
|
15
|
+
* The property that matters, and the reason this is a class rather than a
|
|
16
|
+
* closure over a variable. A screen that loads six panels issues six requests
|
|
17
|
+
* with the same expired access token and gets six `401`s within a few
|
|
18
|
+
* milliseconds of each other. Six refreshes would follow — and because
|
|
19
|
+
* `POST /auth/refresh` **rotates** (Doc 03 §4), five of them would present a
|
|
20
|
+
* token the first has already consumed, which the server is right to treat as
|
|
21
|
+
* replay. One in-flight refresh is therefore not an optimisation but a
|
|
22
|
+
* correctness requirement: {@link TokenSession.refresh} hands every concurrent
|
|
23
|
+
* caller the same promise, and the reuse-detection grace window of Doc 03 §4.1
|
|
24
|
+
* covers only what this cannot.
|
|
25
|
+
*/
|
|
26
|
+
/** The default: tokens last as long as the client object does. */
|
|
27
|
+
export class MemoryTokenStore {
|
|
28
|
+
tokens = null;
|
|
29
|
+
read() {
|
|
30
|
+
return this.tokens;
|
|
31
|
+
}
|
|
32
|
+
write(tokens) {
|
|
33
|
+
this.tokens = tokens;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const DEFAULT_REFRESH_LEEWAY_SECONDS = 30;
|
|
37
|
+
export class TokenSession {
|
|
38
|
+
exchangeRefreshToken;
|
|
39
|
+
store;
|
|
40
|
+
now;
|
|
41
|
+
leewayMs;
|
|
42
|
+
onSessionEnded;
|
|
43
|
+
/** The one in-flight refresh every concurrent caller shares. */
|
|
44
|
+
pending = null;
|
|
45
|
+
constructor(
|
|
46
|
+
/** `POST /auth/refresh`, injected because it goes back through the transport. */
|
|
47
|
+
exchangeRefreshToken, options = {}) {
|
|
48
|
+
this.exchangeRefreshToken = exchangeRefreshToken;
|
|
49
|
+
this.store = options.store ?? new MemoryTokenStore();
|
|
50
|
+
this.now = options.now ?? (() => Date.now());
|
|
51
|
+
this.leewayMs =
|
|
52
|
+
(options.refreshLeewaySeconds ?? DEFAULT_REFRESH_LEEWAY_SECONDS) * 1000;
|
|
53
|
+
this.onSessionEnded = options.onSessionEnded;
|
|
54
|
+
}
|
|
55
|
+
/** What is stored right now, without renewing anything. */
|
|
56
|
+
async tokens() {
|
|
57
|
+
return (await this.store.read()) ?? null;
|
|
58
|
+
}
|
|
59
|
+
async isAuthenticated() {
|
|
60
|
+
return (await this.tokens()) !== null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The token to send, renewed first if it is about to lapse.
|
|
64
|
+
*
|
|
65
|
+
* A failed proactive renewal answers `null` rather than the token it could
|
|
66
|
+
* not replace: the stored pair is gone by then, and sending a token this
|
|
67
|
+
* object no longer holds would produce a `401` whose retry has nothing left
|
|
68
|
+
* to refresh with.
|
|
69
|
+
*/
|
|
70
|
+
async accessToken() {
|
|
71
|
+
const current = await this.tokens();
|
|
72
|
+
if (current === null)
|
|
73
|
+
return null;
|
|
74
|
+
if (current.refreshToken !== null && this.expiringSoon(current)) {
|
|
75
|
+
try {
|
|
76
|
+
return (await this.refresh()).access_token;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return current.accessToken;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The transport's `401` hook.
|
|
86
|
+
*
|
|
87
|
+
* Two things happen here that a plain "refresh on 401" would get wrong. The
|
|
88
|
+
* first is the check that the stored token is still the one that failed: when
|
|
89
|
+
* six requests race, five of them arrive after the refresh has already landed
|
|
90
|
+
* and simply need retrying with what is now stored — no second refresh, and
|
|
91
|
+
* no rotation of a token nobody has used yet. The second is that failure is
|
|
92
|
+
* `false`, not an exception: the caller's original `401` is the honest answer
|
|
93
|
+
* to their request, and replacing it with a refresh error would report the
|
|
94
|
+
* wrong failed call.
|
|
95
|
+
*/
|
|
96
|
+
async reauthorize(usedToken) {
|
|
97
|
+
const current = await this.tokens();
|
|
98
|
+
if (current === null)
|
|
99
|
+
return false;
|
|
100
|
+
if (usedToken !== null && current.accessToken !== usedToken)
|
|
101
|
+
return true;
|
|
102
|
+
if (current.refreshToken === null)
|
|
103
|
+
return false;
|
|
104
|
+
try {
|
|
105
|
+
await this.refresh();
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Renews the pair, sharing one exchange with every concurrent caller.
|
|
114
|
+
*
|
|
115
|
+
* A failed refresh clears the store: the refresh token is either expired,
|
|
116
|
+
* revoked, or has been replayed, and all three mean this session is over
|
|
117
|
+
* (Doc 03 §4.1). Keeping it would guarantee that every later call spends a
|
|
118
|
+
* round trip rediscovering the same thing.
|
|
119
|
+
*/
|
|
120
|
+
async refresh() {
|
|
121
|
+
if (this.pending !== null)
|
|
122
|
+
return this.pending;
|
|
123
|
+
this.pending = this.runRefresh().finally(() => {
|
|
124
|
+
this.pending = null;
|
|
125
|
+
});
|
|
126
|
+
return this.pending;
|
|
127
|
+
}
|
|
128
|
+
async runRefresh() {
|
|
129
|
+
const current = await this.tokens();
|
|
130
|
+
if (current === null || current.refreshToken === null) {
|
|
131
|
+
throw new Error('No refresh token: nothing to renew.');
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const pair = await this.exchangeRefreshToken(current.refreshToken);
|
|
135
|
+
await this.adopt(pair);
|
|
136
|
+
return pair;
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
await this.forget('refresh_failed');
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Takes the tokens a login, refresh or service-token exchange returned. */
|
|
144
|
+
async adopt(tokens) {
|
|
145
|
+
await this.store.write({
|
|
146
|
+
accessToken: tokens.access_token,
|
|
147
|
+
refreshToken: 'refresh_token' in tokens ? tokens.refresh_token : null,
|
|
148
|
+
expiresAt: typeof tokens.expires_in === 'number'
|
|
149
|
+
? this.now() + tokens.expires_in * 1000
|
|
150
|
+
: null,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/** Adopts tokens from somewhere else — a server-rendered page, another tab. */
|
|
154
|
+
async restore(tokens) {
|
|
155
|
+
await this.store.write(tokens);
|
|
156
|
+
}
|
|
157
|
+
/** Drops the tokens locally. The server-side revocation is `POST /auth/logout`. */
|
|
158
|
+
async clear() {
|
|
159
|
+
await this.forget('logout');
|
|
160
|
+
}
|
|
161
|
+
async forget(reason) {
|
|
162
|
+
await this.store.write(null);
|
|
163
|
+
this.onSessionEnded?.(reason);
|
|
164
|
+
}
|
|
165
|
+
expiringSoon(tokens) {
|
|
166
|
+
return tokens.expiresAt !== null && tokens.expiresAt - this.leewayMs <= this.now();
|
|
167
|
+
}
|
|
168
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `IamClient` — one import gives a consumer authenticated, typed IAM access
|
|
3
|
+
* (Doc 08 §2).
|
|
4
|
+
*
|
|
5
|
+
* The composition root, and deliberately the only file that knows all the parts
|
|
6
|
+
* exist: the transport, the token lifecycle, the grants cache and the ten
|
|
7
|
+
* endpoint modules. Everything it wires together is separately constructible, so
|
|
8
|
+
* a consumer with an unusual need — a script that only talks to the registry, a
|
|
9
|
+
* test that wants the cache without a socket — is never forced through this
|
|
10
|
+
* class.
|
|
11
|
+
*
|
|
12
|
+
* ## What it is responsible for that nothing else is
|
|
13
|
+
*
|
|
14
|
+
* Three couplings live here because they exist only when the parts are
|
|
15
|
+
* assembled:
|
|
16
|
+
*
|
|
17
|
+
* 1. The transport asks {@link TokenSession} for a token, and hands it back on a
|
|
18
|
+
* `401` — which is what makes the automatic, single-flight refresh work
|
|
19
|
+
* without any endpoint module knowing about tokens.
|
|
20
|
+
* 2. The refresh call itself goes back out through the transport, so it gets the
|
|
21
|
+
* same base URL, headers, timeout and error mapping as everything else.
|
|
22
|
+
* 3. Any change of identity — login, service-token exchange, logout, refresh —
|
|
23
|
+
* empties the grants cache. Serving one subject's grants to the next is the
|
|
24
|
+
* one caching mistake that would hand a user somebody else's menu.
|
|
25
|
+
*
|
|
26
|
+
* ## Every Doc 06 surface has a typed method
|
|
27
|
+
*
|
|
28
|
+
* `/iam/audit` was the one gap while Session 25's endpoint and its contract
|
|
29
|
+
* types did not exist — a method here would have had to invent both, and that
|
|
30
|
+
* session would then have been implementing against a client rather than against
|
|
31
|
+
* Doc 06. Both landed, and `endpoints/audit.ts` closed it in Session 37.
|
|
32
|
+
*/
|
|
33
|
+
import type { ResolvedGrants, ResolveQuery } from '@plantops/contracts';
|
|
34
|
+
import { TokenSession, type SessionEndReason, type TokenStore, type TokenSessionOptions } from './auth.js';
|
|
35
|
+
import { type ApplicationsApi, type AuditApi, type EntitlementsApi, type AuthApi, type ClientsApi, type NavigationApi, type PermissionsApi, type RoleBindingsApi, type RolesApi, type ScopesApi, type ServiceAccountsApi, type UsersApi } from './endpoints/index.js';
|
|
36
|
+
import { type FetchLike, type Requester } from './http.js';
|
|
37
|
+
export interface IamClientOptions {
|
|
38
|
+
/** The API root — the origin, without the `/iam` or `/auth` prefix. */
|
|
39
|
+
baseUrl: string;
|
|
40
|
+
/** Defaults to the runtime's global. Supply one to test, or to instrument. */
|
|
41
|
+
fetch?: FetchLike;
|
|
42
|
+
/** Sent on every request — a correlation header, a user agent. */
|
|
43
|
+
headers?: Readonly<Record<string, string>>;
|
|
44
|
+
/** Abort a request that has taken this long. Omit for no client-side limit. */
|
|
45
|
+
timeoutMs?: number;
|
|
46
|
+
/** Where tokens live. Defaults to memory; a browser supplies its own. */
|
|
47
|
+
tokenStore?: TokenStore;
|
|
48
|
+
/** Renew this many seconds before the access token lapses. Default 30. */
|
|
49
|
+
refreshLeewaySeconds?: TokenSessionOptions['refreshLeewaySeconds'];
|
|
50
|
+
/** How long {@link IamClient.grants} may serve a cached answer. Default 60. */
|
|
51
|
+
resolveCacheTtlSeconds?: number;
|
|
52
|
+
/** Injectable clock, shared by the token expiry and the cache. */
|
|
53
|
+
now?: () => number;
|
|
54
|
+
/** The session ended: the caller logged out, or a refresh was refused. */
|
|
55
|
+
onSessionEnded?: (reason: SessionEndReason) => void;
|
|
56
|
+
}
|
|
57
|
+
export declare class IamClient {
|
|
58
|
+
/** The token lifecycle: read, restore or clear credentials directly. */
|
|
59
|
+
readonly session: TokenSession;
|
|
60
|
+
readonly auth: AuthApi;
|
|
61
|
+
readonly applications: ApplicationsApi;
|
|
62
|
+
readonly clients: ClientsApi;
|
|
63
|
+
readonly scopes: ScopesApi;
|
|
64
|
+
readonly roles: RolesApi;
|
|
65
|
+
readonly users: UsersApi;
|
|
66
|
+
readonly roleBindings: RoleBindingsApi;
|
|
67
|
+
readonly serviceAccounts: ServiceAccountsApi;
|
|
68
|
+
readonly permissions: PermissionsApi;
|
|
69
|
+
readonly navigation: NavigationApi;
|
|
70
|
+
readonly audit: AuditApi;
|
|
71
|
+
/**
|
|
72
|
+
* Term, ceilings and per-application entitlement.
|
|
73
|
+
*
|
|
74
|
+
* Deliberately not folded into {@link IamClient.grants}: grants invalidate on
|
|
75
|
+
* a version counter, entitlements go stale on a clock, and one cache cannot
|
|
76
|
+
* serve both. See `endpoints/entitlements.ts`.
|
|
77
|
+
*/
|
|
78
|
+
readonly entitlements: EntitlementsApi;
|
|
79
|
+
/**
|
|
80
|
+
* The raw, authenticated request function.
|
|
81
|
+
*
|
|
82
|
+
* The escape hatch for an endpoint this library does not yet type. Nothing on
|
|
83
|
+
* Doc 06's surface needs it today; it stays because the alternative, when the
|
|
84
|
+
* next route arrives ahead of its method, is a consumer abandoning the token
|
|
85
|
+
* handling and error mapping for all of them.
|
|
86
|
+
*/
|
|
87
|
+
readonly request: Requester;
|
|
88
|
+
private readonly grantsCache;
|
|
89
|
+
constructor(options: IamClientOptions);
|
|
90
|
+
/**
|
|
91
|
+
* The bearer's grants, cached (Doc 06 §11).
|
|
92
|
+
*
|
|
93
|
+
* What a module's `PermissionGuard` should call on every gated request: the
|
|
94
|
+
* burst of authorizations one request fan-out produces collapses into a single
|
|
95
|
+
* resolve, and the answer is reused for the cache's lifetime.
|
|
96
|
+
*/
|
|
97
|
+
grants(query?: ResolveQuery): Promise<ResolvedGrants>;
|
|
98
|
+
/** Re-resolves now, ignoring the cache — after a change the caller just made. */
|
|
99
|
+
refreshGrants(query?: ResolveQuery): Promise<ResolvedGrants>;
|
|
100
|
+
/**
|
|
101
|
+
* Drops cached grants — for one application, or all of them.
|
|
102
|
+
*
|
|
103
|
+
* The hook a consumer subscribed to `perms.invalidated` (Doc 04 §7) calls, and
|
|
104
|
+
* the reason such a consumer may safely raise `resolveCacheTtlSeconds`.
|
|
105
|
+
*/
|
|
106
|
+
invalidateGrants(applicationId?: string): void;
|
|
107
|
+
}
|
|
108
|
+
/** `createIamClient({ baseUrl })` — the same thing, for consumers that prefer it. */
|
|
109
|
+
export declare function createIamClient(options: IamClientOptions): IamClient;
|
|
110
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAqB,MAAM,qBAAqB,CAAC;AAG3F,OAAO,EACL,YAAY,EACZ,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,mBAAmB,EACzB,MAAM,WAAW,CAAC;AACnB,OAAO,EAaL,KAAK,eAAe,EACpB,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,OAAO,EACZ,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,kBAAkB,EACvB,KAAK,QAAQ,EACd,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAiB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAG1E,MAAM,WAAW,gBAAgB;IAC/B,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,kEAAkE;IAClE,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3C,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,0EAA0E;IAC1E,oBAAoB,CAAC,EAAE,mBAAmB,CAAC,sBAAsB,CAAC,CAAC;IACnE,+EAA+E;IAC/E,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,kEAAkE;IAClE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,0EAA0E;IAC1E,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;CACrD;AAED,qBAAa,SAAS;IACpB,wEAAwE;IACxE,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAE/B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,eAAe,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,eAAe,CAAC;IACvC,QAAQ,CAAC,eAAe,EAAE,kBAAkB,CAAC;IAC7C,QAAQ,CAAC,WAAW,EAAE,cAAc,CAAC;IACrC,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,EAAE,eAAe,CAAC;IAEvC;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC;IAE5B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAe;gBAE/B,OAAO,EAAE,gBAAgB;IAgDrC;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,GAAE,YAAiB,GAAG,OAAO,CAAC,cAAc,CAAC;IAIzD,iFAAiF;IACjF,aAAa,CAAC,KAAK,GAAE,YAAiB,GAAG,OAAO,CAAC,cAAc,CAAC;IAIhE;;;;;OAKG;IACH,gBAAgB,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI;CAG/C;AAED,qFAAqF;AACrF,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,SAAS,CAEpE"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `IamClient` — one import gives a consumer authenticated, typed IAM access
|
|
3
|
+
* (Doc 08 §2).
|
|
4
|
+
*
|
|
5
|
+
* The composition root, and deliberately the only file that knows all the parts
|
|
6
|
+
* exist: the transport, the token lifecycle, the grants cache and the ten
|
|
7
|
+
* endpoint modules. Everything it wires together is separately constructible, so
|
|
8
|
+
* a consumer with an unusual need — a script that only talks to the registry, a
|
|
9
|
+
* test that wants the cache without a socket — is never forced through this
|
|
10
|
+
* class.
|
|
11
|
+
*
|
|
12
|
+
* ## What it is responsible for that nothing else is
|
|
13
|
+
*
|
|
14
|
+
* Three couplings live here because they exist only when the parts are
|
|
15
|
+
* assembled:
|
|
16
|
+
*
|
|
17
|
+
* 1. The transport asks {@link TokenSession} for a token, and hands it back on a
|
|
18
|
+
* `401` — which is what makes the automatic, single-flight refresh work
|
|
19
|
+
* without any endpoint module knowing about tokens.
|
|
20
|
+
* 2. The refresh call itself goes back out through the transport, so it gets the
|
|
21
|
+
* same base URL, headers, timeout and error mapping as everything else.
|
|
22
|
+
* 3. Any change of identity — login, service-token exchange, logout, refresh —
|
|
23
|
+
* empties the grants cache. Serving one subject's grants to the next is the
|
|
24
|
+
* one caching mistake that would hand a user somebody else's menu.
|
|
25
|
+
*
|
|
26
|
+
* ## Every Doc 06 surface has a typed method
|
|
27
|
+
*
|
|
28
|
+
* `/iam/audit` was the one gap while Session 25's endpoint and its contract
|
|
29
|
+
* types did not exist — a method here would have had to invent both, and that
|
|
30
|
+
* session would then have been implementing against a client rather than against
|
|
31
|
+
* Doc 06. Both landed, and `endpoints/audit.ts` closed it in Session 37.
|
|
32
|
+
*/
|
|
33
|
+
import { AUTH_ROUTE_PREFIX } from '@plantops/contracts';
|
|
34
|
+
import { TokenSession, } from './auth.js';
|
|
35
|
+
import { applicationsEndpoints, auditEndpoints, entitlementsEndpoints, authEndpoints, clientsEndpoints, navigationEndpoints, permissionsEndpoints, roleBindingsEndpoints, rolesEndpoints, scopesEndpoints, serviceAccountsEndpoints, usersEndpoints, } from './endpoints/index.js';
|
|
36
|
+
import { HttpTransport } from './http.js';
|
|
37
|
+
import { ResolveCache } from './resolve-cache.js';
|
|
38
|
+
export class IamClient {
|
|
39
|
+
/** The token lifecycle: read, restore or clear credentials directly. */
|
|
40
|
+
session;
|
|
41
|
+
auth;
|
|
42
|
+
applications;
|
|
43
|
+
clients;
|
|
44
|
+
scopes;
|
|
45
|
+
roles;
|
|
46
|
+
users;
|
|
47
|
+
roleBindings;
|
|
48
|
+
serviceAccounts;
|
|
49
|
+
permissions;
|
|
50
|
+
navigation;
|
|
51
|
+
audit;
|
|
52
|
+
/**
|
|
53
|
+
* Term, ceilings and per-application entitlement.
|
|
54
|
+
*
|
|
55
|
+
* Deliberately not folded into {@link IamClient.grants}: grants invalidate on
|
|
56
|
+
* a version counter, entitlements go stale on a clock, and one cache cannot
|
|
57
|
+
* serve both. See `endpoints/entitlements.ts`.
|
|
58
|
+
*/
|
|
59
|
+
entitlements;
|
|
60
|
+
/**
|
|
61
|
+
* The raw, authenticated request function.
|
|
62
|
+
*
|
|
63
|
+
* The escape hatch for an endpoint this library does not yet type. Nothing on
|
|
64
|
+
* Doc 06's surface needs it today; it stays because the alternative, when the
|
|
65
|
+
* next route arrives ahead of its method, is a consumer abandoning the token
|
|
66
|
+
* handling and error mapping for all of them.
|
|
67
|
+
*/
|
|
68
|
+
request;
|
|
69
|
+
grantsCache;
|
|
70
|
+
constructor(options) {
|
|
71
|
+
const transport = new HttpTransport({
|
|
72
|
+
baseUrl: options.baseUrl,
|
|
73
|
+
fetch: options.fetch,
|
|
74
|
+
headers: options.headers,
|
|
75
|
+
timeoutMs: options.timeoutMs,
|
|
76
|
+
authorize: () => this.session.accessToken(),
|
|
77
|
+
reauthorize: (usedToken) => this.session.reauthorize(usedToken),
|
|
78
|
+
});
|
|
79
|
+
this.request = transport.request;
|
|
80
|
+
this.session = new TokenSession((refreshToken) => this.request({
|
|
81
|
+
method: 'POST',
|
|
82
|
+
path: `${AUTH_ROUTE_PREFIX}/refresh`,
|
|
83
|
+
body: { refresh_token: refreshToken },
|
|
84
|
+
auth: 'none',
|
|
85
|
+
}), {
|
|
86
|
+
store: options.tokenStore,
|
|
87
|
+
now: options.now,
|
|
88
|
+
refreshLeewaySeconds: options.refreshLeewaySeconds,
|
|
89
|
+
onSessionEnded: options.onSessionEnded,
|
|
90
|
+
});
|
|
91
|
+
this.permissions = permissionsEndpoints(this.request);
|
|
92
|
+
this.grantsCache = new ResolveCache((query) => this.permissions.resolve(query), {
|
|
93
|
+
ttlSeconds: options.resolveCacheTtlSeconds,
|
|
94
|
+
now: options.now,
|
|
95
|
+
});
|
|
96
|
+
this.auth = authEndpoints(this.request, this.session, () => this.grantsCache.clear());
|
|
97
|
+
this.applications = applicationsEndpoints(this.request);
|
|
98
|
+
this.clients = clientsEndpoints(this.request);
|
|
99
|
+
this.scopes = scopesEndpoints(this.request);
|
|
100
|
+
this.roles = rolesEndpoints(this.request);
|
|
101
|
+
this.users = usersEndpoints(this.request);
|
|
102
|
+
this.roleBindings = roleBindingsEndpoints(this.request);
|
|
103
|
+
this.serviceAccounts = serviceAccountsEndpoints(this.request);
|
|
104
|
+
this.navigation = navigationEndpoints(this.request);
|
|
105
|
+
this.audit = auditEndpoints(this.request);
|
|
106
|
+
this.entitlements = entitlementsEndpoints(this.request);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The bearer's grants, cached (Doc 06 §11).
|
|
110
|
+
*
|
|
111
|
+
* What a module's `PermissionGuard` should call on every gated request: the
|
|
112
|
+
* burst of authorizations one request fan-out produces collapses into a single
|
|
113
|
+
* resolve, and the answer is reused for the cache's lifetime.
|
|
114
|
+
*/
|
|
115
|
+
grants(query = {}) {
|
|
116
|
+
return this.grantsCache.get(query);
|
|
117
|
+
}
|
|
118
|
+
/** Re-resolves now, ignoring the cache — after a change the caller just made. */
|
|
119
|
+
refreshGrants(query = {}) {
|
|
120
|
+
return this.grantsCache.reload(query);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Drops cached grants — for one application, or all of them.
|
|
124
|
+
*
|
|
125
|
+
* The hook a consumer subscribed to `perms.invalidated` (Doc 04 §7) calls, and
|
|
126
|
+
* the reason such a consumer may safely raise `resolveCacheTtlSeconds`.
|
|
127
|
+
*/
|
|
128
|
+
invalidateGrants(applicationId) {
|
|
129
|
+
this.grantsCache.invalidate(applicationId);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** `createIamClient({ baseUrl })` — the same thing, for consumers that prefer it. */
|
|
133
|
+
export function createIamClient(options) {
|
|
134
|
+
return new IamClient(options);
|
|
135
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/iam/applications/*` — the platform application registry, Doc 06 §4.
|
|
3
|
+
*
|
|
4
|
+
* The four catalog routes and the manifest that replaces them are all here
|
|
5
|
+
* because they are one screen's worth of API (Doc 09 §2): an application, its
|
|
6
|
+
* permissions, its nav nodes, and the mapping between the last two.
|
|
7
|
+
*/
|
|
8
|
+
import type { ApplicationDTO, ApplicationManifest, CreateApplicationRequest, CreateNavNodesRequest, CreatePermissionsRequest, ManifestUpsertResponse, NavCatalogResponse, NavNodeCatalogDTO, NavPermissionsRequest, NavPermissionsResult, Paginated, PaginationQuery, PermissionDTO, UpdateApplicationRequest } from '@plantops/contracts';
|
|
9
|
+
import type { Requester } from '../http.js';
|
|
10
|
+
export interface ApplicationsApi {
|
|
11
|
+
create(body: CreateApplicationRequest): Promise<ApplicationDTO>;
|
|
12
|
+
list(query?: PaginationQuery): Promise<Paginated<ApplicationDTO>>;
|
|
13
|
+
/** Update, or the global on/off switch of Doc 02 §7. */
|
|
14
|
+
update(id: string, body: UpdateApplicationRequest): Promise<ApplicationDTO>;
|
|
15
|
+
addPermissions(id: string, body: CreatePermissionsRequest): Promise<PermissionDTO[]>;
|
|
16
|
+
listPermissions(id: string, query?: PaginationQuery): Promise<Paginated<PermissionDTO>>;
|
|
17
|
+
addNavNodes(id: string, body: CreateNavNodesRequest): Promise<NavNodeCatalogDTO[]>;
|
|
18
|
+
navTree(id: string): Promise<NavCatalogResponse>;
|
|
19
|
+
mapNavPermissions(id: string, body: NavPermissionsRequest): Promise<NavPermissionsResult>;
|
|
20
|
+
/**
|
|
21
|
+
* Removes mappings. Doc 06 §4's table stops at the POST; the DELETE exists
|
|
22
|
+
* because a mapping added by mistake has to be removable without a manifest
|
|
23
|
+
* upload (see `registry/nav.service.ts`).
|
|
24
|
+
*/
|
|
25
|
+
unmapNavPermissions(id: string, body: NavPermissionsRequest): Promise<NavPermissionsResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Idempotent upsert of the whole catalog from an application's manifest
|
|
28
|
+
* (Doc 02 §2). The body is the manifest document itself, unwrapped, so the
|
|
29
|
+
* file on disk and the body on the wire are the same thing.
|
|
30
|
+
*/
|
|
31
|
+
upsertManifest(id: string, manifest: ApplicationManifest): Promise<ManifestUpsertResponse>;
|
|
32
|
+
/**
|
|
33
|
+
* What {@link ApplicationsApi.upsertManifest} *would* do — `?dryRun=true`.
|
|
34
|
+
*
|
|
35
|
+
* The preview behind Doc 09 §2.1's upload screen: the same validation, the
|
|
36
|
+
* same refusals and the same `ManifestDiff`, computed against the catalog as
|
|
37
|
+
* it stands and applied to nothing. `dry_run` comes back `true`, and `changed`
|
|
38
|
+
* answers whether confirming would do anything at all.
|
|
39
|
+
*
|
|
40
|
+
* A separate method rather than an options argument, because the difference
|
|
41
|
+
* between the two calls is whether they write. A boolean parameter that
|
|
42
|
+
* decides that reads the same at both call sites and is the wrong thing to get
|
|
43
|
+
* backwards.
|
|
44
|
+
*/
|
|
45
|
+
previewManifest(id: string, manifest: ApplicationManifest): Promise<ManifestUpsertResponse>;
|
|
46
|
+
}
|
|
47
|
+
export declare function applicationsEndpoints(request: Requester): ApplicationsApi;
|
|
48
|
+
//# sourceMappingURL=applications.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"applications.d.ts","sourceRoot":"","sources":["../../src/endpoints/applications.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,mBAAmB,EACnB,wBAAwB,EACxB,qBAAqB,EACrB,wBAAwB,EACxB,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,qBAAqB,EACrB,oBAAoB,EACpB,SAAS,EACT,eAAe,EACf,aAAa,EACb,wBAAwB,EACzB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,IAAI,EAAE,wBAAwB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IAChE,IAAI,CAAC,KAAK,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC,CAAC;IAClE,wDAAwD;IACxD,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,wBAAwB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IAE5E,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,wBAAwB,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;IACrF,eAAe,CACb,EAAE,EAAE,MAAM,EACV,KAAK,CAAC,EAAE,eAAe,GACtB,OAAO,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC;IAErC,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;IACnF,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACjD,iBAAiB,CACf,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,qBAAqB,GAC1B,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACjC;;;;OAIG;IACH,mBAAmB,CACjB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,qBAAqB,GAC1B,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAEjC;;;;OAIG;IACH,cAAc,CACZ,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,mBAAmB,GAC5B,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAEnC;;;;;;;;;;;;OAYG;IACH,eAAe,CACb,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,mBAAmB,GAC5B,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACpC;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,SAAS,GAAG,eAAe,CAiCzE"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/iam/applications/*` — the platform application registry, Doc 06 §4.
|
|
3
|
+
*
|
|
4
|
+
* The four catalog routes and the manifest that replaces them are all here
|
|
5
|
+
* because they are one screen's worth of API (Doc 09 §2): an application, its
|
|
6
|
+
* permissions, its nav nodes, and the mapping between the last two.
|
|
7
|
+
*/
|
|
8
|
+
import { IAM_ROUTE_PREFIX } from '@plantops/contracts';
|
|
9
|
+
export function applicationsEndpoints(request) {
|
|
10
|
+
const base = `${IAM_ROUTE_PREFIX}/applications`;
|
|
11
|
+
const at = (id, suffix = '') => `${base}/${encodeURIComponent(id)}${suffix}`;
|
|
12
|
+
return {
|
|
13
|
+
create: (body) => request({ method: 'POST', path: base, body }),
|
|
14
|
+
list: (query) => request({ method: 'GET', path: base, query: { ...query } }),
|
|
15
|
+
update: (id, body) => request({ method: 'PATCH', path: at(id), body }),
|
|
16
|
+
addPermissions: (id, body) => request({ method: 'POST', path: at(id, '/permissions'), body }),
|
|
17
|
+
listPermissions: (id, query) => request({ method: 'GET', path: at(id, '/permissions'), query: { ...query } }),
|
|
18
|
+
addNavNodes: (id, body) => request({ method: 'POST', path: at(id, '/nav'), body }),
|
|
19
|
+
navTree: (id) => request({ method: 'GET', path: at(id, '/nav') }),
|
|
20
|
+
mapNavPermissions: (id, body) => request({ method: 'POST', path: at(id, '/nav-permissions'), body }),
|
|
21
|
+
unmapNavPermissions: (id, body) => request({ method: 'DELETE', path: at(id, '/nav-permissions'), body }),
|
|
22
|
+
upsertManifest: (id, manifest) => request({ method: 'POST', path: at(id, '/manifest'), body: manifest }),
|
|
23
|
+
previewManifest: (id, manifest) => request({
|
|
24
|
+
method: 'POST',
|
|
25
|
+
path: at(id, '/manifest'),
|
|
26
|
+
query: { dryRun: true },
|
|
27
|
+
body: manifest,
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
}
|