@mcp-abap-adt/auth-broker 4.0.0 → 5.0.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/dist/bindingOf.js CHANGED
@@ -7,47 +7,77 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.destinationBinding = destinationBinding;
10
+ exports.consumerRow = consumerRow;
10
11
  exports.bindingOf = bindingOf;
11
12
  const binding_1 = require("./binding");
12
13
  const destinations_1 = require("./destinations");
13
14
  /**
14
- * The binding of a `jwt` or `saml` destination with the grant it states: the
15
- * UAA grants and `saml2_bearer` by `uaaUrl` and the client, the OIDC grants by
16
- * the issuer and the client, `saml2_pure` by its ACS, and `none` by what the
17
- * means state.
15
+ * The binding of a `jwt` or `saml` destination with the grant it states — a
16
+ * pair of the closed list, checked before (`statedGrant`): the UAA grants,
17
+ * the OIDC grants, `saml2_pure`, `saml2_bearer` and `none`, each with the
18
+ * fields of its row. `certificate` is the certificate client the build
19
+ * read for the `clientAuthentication` strategy, or `null` when it read none.
18
20
  */
19
- function destinationBinding(authType, grant, means, client) {
21
+ function destinationBinding(authType, grant, means, client, certificate = null) {
20
22
  if (grant === 'none')
21
23
  return (0, binding_1.handedOverBinding)(authType, means, client);
22
- if ((0, destinations_1.isOidcGrant)(grant))
23
- return (0, binding_1.oidcBinding)(means, client);
24
+ if ((0, destinations_1.isOidcGrant)(grant)) {
25
+ return (0, binding_1.oidcBinding)(grant, means, client, certificate);
26
+ }
24
27
  if (grant === 'saml2_pure')
25
28
  return (0, binding_1.samlPureBinding)(means);
26
- return (0, binding_1.uaaBinding)(means, client);
29
+ if (grant === 'saml2_bearer') {
30
+ return (0, binding_1.samlBearerBinding)(means, client, certificate);
31
+ }
32
+ return (0, binding_1.uaaBinding)(grant, means, client, certificate);
33
+ }
34
+ /**
35
+ * The row of the token API's consumer provider: `provider/<authType>/<grant>`
36
+ * when the means state a pair of the closed list, else `provider/-`.
37
+ */
38
+ function consumerRow(means) {
39
+ const authType = means?.authType;
40
+ const grant = means?.grantType;
41
+ return (0, destinations_1.isStatedRow)(authType, grant)
42
+ ? `provider/${authType}/${grant}`
43
+ : 'provider/-';
27
44
  }
28
45
  /**
29
46
  * The binding the broker writes beside a secret for a destination with these
30
- * means and this client — canonical, exactly what `getProvider` compares a
31
- * stored secret against. For a consumer that hands over a credential the
32
- * broker cannot obtain (a `none` destination: a token or cookies it was
33
- * given) and writes it to the session store itself: written with this binding,
34
- * `getProvider` presents it; written with any other, it refuses.
47
+ * means and this client: `issuedFor` the canonical resource, `issuedBy` the
48
+ * version-2 record of the row the means state, built from `means` and
49
+ * `client` alone.
50
+ *
51
+ * It is exactly what `getProvider` compares for a `none` row — its use: a
52
+ * consumer that hands over a credential the broker cannot obtain (a token or
53
+ * cookies it was given) writes it with this binding, and `getProvider`
54
+ * presents it; written with any other — a 4.x `bindingOf`'s included — it
55
+ * refuses. It is also exactly what `getProvider` writes for a token row built
56
+ * without a `clientAuthentication` strategy.
57
+ *
58
+ * It is **not** what a token row writes beside a `clientAuthentication`
59
+ * strategy: there the row's record also holds what the build read of the
60
+ * certificate client — its `certUrl`, and its certificate in the trust
61
+ * digest — and its client identity may be the certificate client's, none of
62
+ * which `bindingOf` is given. Nor is it the token API's record for a
63
+ * consumer's provider (`provider/…`).
35
64
  *
36
65
  * `means` are the key store's (`authType`, `grantType`, `serviceUrl`,
37
- * `sapClient`, the issuer or ACS); `client` is the key store's client, when the
38
- * destination has one. A destination that states no `jwt` / `saml` type or no
39
- * grant binds nothing: `{}`. Never logs, never throws.
66
+ * `sapClient`, the addresses and trust the row reads); `client` is the key
67
+ * store's client, when the destination has one. A destination that states no
68
+ * `jwt` / `saml` type, no grant, or a pair outside the closed list binds
69
+ * nothing: `{}`. Never logs, never throws.
40
70
  */
41
71
  function bindingOf(means, client = null) {
42
72
  const authType = means.authType;
43
73
  const grant = means.grantType;
44
- if ((authType !== 'jwt' && authType !== 'saml') || !grant)
74
+ if (authType !== 'jwt' && authType !== 'saml')
75
+ return {};
76
+ if (!(0, destinations_1.isStatedRow)(authType, grant))
45
77
  return {};
46
78
  const { issuedFor, issuedBy } = destinationBinding(authType, grant, means, client);
47
- const binding = {};
79
+ const binding = { issuedBy };
48
80
  if (issuedFor !== undefined)
49
81
  binding.issuedFor = issuedFor;
50
- if (issuedBy !== undefined)
51
- binding.issuedBy = issuedBy;
52
82
  return binding;
53
83
  }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * A build's identity: everything the build read to make its provider.
3
+ *
4
+ * A provider is never changed: the broker hands out the provider it built
5
+ * while nothing that build read has changed, and builds a new one — which
6
+ * starts with nothing — the moment anything has. "What the build read" is
7
+ * recorded by the build itself, through one recording accessor
8
+ * (`IdentityRecorder`): every store answer a build takes — the means, the
9
+ * client, the certificate client, and on the consumer path the session's
10
+ * connection and client — is handed to the builders behind a proxy that notes
11
+ * each field read, as given. So the set of fields is each row's own, decided
12
+ * by the code that builds it, and cannot drift from it: a field a builder
13
+ * starts to read is part of the identity from then on.
14
+ *
15
+ * Every call re-reads the same sources (`StoreReads`, each at most once per
16
+ * call) and compares each recorded field exactly (`BuildIdentity.unchanged`):
17
+ * strings, numbers and booleans by `Object.is` — an absent value is distinct
18
+ * from `''` — arrays and plain objects element by element.
19
+ *
20
+ * Secrets take part here only: a password, a client secret, a key, a subject
21
+ * token the build read are held in memory beside the provider, which holds
22
+ * them anyway. The identity is never logged, never persisted, and never
23
+ * hashed into anything persisted; what a session file holds is the binding
24
+ * record (`binding.ts`), which holds no secret.
25
+ */
26
+ /** The store answers a build may read. */
27
+ export type SourceName =
28
+ /** The key store's connection config: the means. */
29
+ 'means'
30
+ /** The key store's authorization config: the secret client. */
31
+ | 'client'
32
+ /** The key store's certificate client. */
33
+ | 'certificate'
34
+ /** The session store's connection config (the consumer path). */
35
+ | 'sessionConnection'
36
+ /** The session store's authorization config (the consumer path). */
37
+ | 'sessionClient';
38
+ /** Reads one source from its store: an object, or `null` for none. */
39
+ export type SourceReader = (name: SourceName) => Promise<object | null>;
40
+ /**
41
+ * The error for a store answer whose `field` the broker cannot take — a
42
+ * function, a symbol, a getter that throws: a configuration fault naming the
43
+ * field, never its value.
44
+ */
45
+ export type ShapeRefusal = (field: string) => unknown;
46
+ /**
47
+ * The sources of one call, each read from its store at most once: the
48
+ * comparison with the cached build and — when that finds a change — the new
49
+ * build read the same answers.
50
+ */
51
+ export declare class StoreReads {
52
+ private readonly reader;
53
+ private readonly refuse;
54
+ private readonly answers;
55
+ /**
56
+ * @param refuse The error for a field of a shape the broker cannot take.
57
+ */
58
+ constructor(reader: SourceReader, refuse: ShapeRefusal);
59
+ read(name: SourceName): Promise<object | null>;
60
+ }
61
+ /** One fact a build read. */
62
+ type Entry = {
63
+ readonly source: SourceName;
64
+ readonly kind: 'answer';
65
+ readonly value: 'none' | 'some' | 'failed';
66
+ } | {
67
+ readonly source: SourceName;
68
+ readonly kind: 'get';
69
+ readonly field: string;
70
+ readonly value: unknown;
71
+ } | {
72
+ readonly source: SourceName;
73
+ readonly kind: 'has';
74
+ readonly field: string;
75
+ readonly value: boolean;
76
+ } | {
77
+ readonly source: SourceName;
78
+ readonly kind: 'keys';
79
+ readonly value: readonly string[];
80
+ } | {
81
+ readonly kind: 'derived';
82
+ readonly value: unknown;
83
+ readonly derive: (reads: StoreReads) => Promise<unknown>;
84
+ };
85
+ /**
86
+ * Exact equality of two values as read: `Object.is` for everything but arrays
87
+ * and plain objects, which are equal element by element (and key by key).
88
+ */
89
+ export declare function sameValue(a: unknown, b: unknown): boolean;
90
+ /**
91
+ * The one recording accessor of a build: every source the build reads goes
92
+ * through `read`, and every field read of what it answers — a property read,
93
+ * an `in` test, an enumeration — is noted, once, as given. `seal()` ends the
94
+ * recording: what is read afterwards is not the build's.
95
+ */
96
+ export declare class IdentityRecorder {
97
+ readonly reads: StoreReads;
98
+ private readonly entries;
99
+ private readonly noted;
100
+ private sealed;
101
+ /** @param reads The call's reads, which every recorded source goes through. */
102
+ constructor(reads: StoreReads);
103
+ /**
104
+ * The source's answer, behind the recording proxy, or `null`. A read that
105
+ * fails is noted as failed and rethrown.
106
+ */
107
+ read<T extends object>(name: SourceName): Promise<T | null>;
108
+ /**
109
+ * Notes a value the build derived from sources it reads without the
110
+ * recording proxy, with how to derive it again from a later call's reads.
111
+ * For sources whose answers change for reasons that are no change of the
112
+ * means — the session store's, which the broker's own writes change: the
113
+ * identity is what the build took from them, never what else they hold.
114
+ */
115
+ derived(value: unknown, derive: (reads: StoreReads) => Promise<unknown>): void;
116
+ /** The identity as read so far; nothing read later is recorded. */
117
+ seal(): BuildIdentity;
118
+ private note;
119
+ private watched;
120
+ }
121
+ /**
122
+ * What one build read, sealed: compared with a fresh read of the same
123
+ * sources on every call. Holds secrets the build read, in memory only.
124
+ */
125
+ export declare class BuildIdentity {
126
+ private readonly entries;
127
+ constructor(entries: readonly Entry[]);
128
+ /**
129
+ * Whether every fact this build read is still what the stores answer —
130
+ * each source re-read through `reads` (once per call), each field compared
131
+ * exactly. A source that cannot be read now, where it could then, is a
132
+ * change: the new build meets the failure itself.
133
+ */
134
+ unchanged(reads: StoreReads): Promise<boolean>;
135
+ }
136
+ export {};
137
+ //# sourceMappingURL=buildIdentity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildIdentity.d.ts","sourceRoot":"","sources":["../src/buildIdentity.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,393 @@
1
+ "use strict";
2
+ /**
3
+ * A build's identity: everything the build read to make its provider.
4
+ *
5
+ * A provider is never changed: the broker hands out the provider it built
6
+ * while nothing that build read has changed, and builds a new one — which
7
+ * starts with nothing — the moment anything has. "What the build read" is
8
+ * recorded by the build itself, through one recording accessor
9
+ * (`IdentityRecorder`): every store answer a build takes — the means, the
10
+ * client, the certificate client, and on the consumer path the session's
11
+ * connection and client — is handed to the builders behind a proxy that notes
12
+ * each field read, as given. So the set of fields is each row's own, decided
13
+ * by the code that builds it, and cannot drift from it: a field a builder
14
+ * starts to read is part of the identity from then on.
15
+ *
16
+ * Every call re-reads the same sources (`StoreReads`, each at most once per
17
+ * call) and compares each recorded field exactly (`BuildIdentity.unchanged`):
18
+ * strings, numbers and booleans by `Object.is` — an absent value is distinct
19
+ * from `''` — arrays and plain objects element by element.
20
+ *
21
+ * Secrets take part here only: a password, a client secret, a key, a subject
22
+ * token the build read are held in memory beside the provider, which holds
23
+ * them anyway. The identity is never logged, never persisted, and never
24
+ * hashed into anything persisted; what a session file holds is the binding
25
+ * record (`binding.ts`), which holds no secret.
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.BuildIdentity = exports.IdentityRecorder = exports.StoreReads = void 0;
29
+ exports.sameValue = sameValue;
30
+ /** `IConnectionConfig`'s fields (interfaces-auth-broker). */
31
+ const CONNECTION_FIELDS = [
32
+ 'serviceUrl',
33
+ 'authorizationToken',
34
+ 'username',
35
+ 'password',
36
+ 'authType',
37
+ 'grantType',
38
+ 'expiresAt',
39
+ 'issuedFor',
40
+ 'issuedBy',
41
+ 'sapClient',
42
+ 'language',
43
+ 'sessionCookies',
44
+ 'sncPartnerName',
45
+ 'sncQop',
46
+ 'sncLib',
47
+ 'sncMyName',
48
+ 'oidcIssuerUrl',
49
+ 'oidcAuthorizationEndpoint',
50
+ 'oidcTokenEndpoint',
51
+ 'oidcDeviceAuthorizationEndpoint',
52
+ 'oidcScopes',
53
+ 'oidcSubjectToken',
54
+ 'oidcSubjectTokenType',
55
+ 'oidcAudience',
56
+ 'oidcActorToken',
57
+ 'oidcActorTokenType',
58
+ 'samlIdpSsoUrl',
59
+ 'samlIdpEntityId',
60
+ 'samlIdpCertificates',
61
+ 'samlSpEntityId',
62
+ 'samlAcsUrl',
63
+ 'samlRelayState',
64
+ 'samlIdpInitiated',
65
+ 'samlClockSkewMs',
66
+ 'samlTokenUrl',
67
+ ];
68
+ /** `IAuthorizationConfig`'s fields (interfaces-auth-sap). */
69
+ const AUTHORIZATION_FIELDS = [
70
+ 'uaaUrl',
71
+ 'uaaClientId',
72
+ 'uaaClientSecret',
73
+ 'refreshToken',
74
+ ];
75
+ /** `IClientCertificate`'s fields (interfaces-auth-broker). */
76
+ const CERTIFICATE_FIELDS = [
77
+ 'uaaUrl',
78
+ 'clientId',
79
+ 'certificate',
80
+ 'key',
81
+ 'certUrl',
82
+ ];
83
+ /** The contract each source answers, by its fields. */
84
+ const CONTRACT_FIELDS = {
85
+ means: CONNECTION_FIELDS,
86
+ sessionConnection: CONNECTION_FIELDS,
87
+ client: AUTHORIZATION_FIELDS,
88
+ sessionClient: AUTHORIZATION_FIELDS,
89
+ certificate: CERTIFICATE_FIELDS,
90
+ };
91
+ /**
92
+ * The sources of one call, each read from its store at most once: the
93
+ * comparison with the cached build and — when that finds a change — the new
94
+ * build read the same answers.
95
+ */
96
+ class StoreReads {
97
+ reader;
98
+ refuse;
99
+ answers = new Map();
100
+ /**
101
+ * @param refuse The error for a field of a shape the broker cannot take.
102
+ */
103
+ constructor(reader, refuse) {
104
+ this.reader = reader;
105
+ this.refuse = refuse;
106
+ }
107
+ read(name) {
108
+ let answer = this.answers.get(name);
109
+ if (!answer) {
110
+ // One snapshot per answer: what the builders hand their providers and
111
+ // what the identity records are the same values, and a store that
112
+ // changes its object afterwards — in place — changes neither.
113
+ answer = Promise.resolve()
114
+ .then(() => this.reader(name))
115
+ .then((value) => snapshot(name, value, this.refuse));
116
+ // Read by whoever asks; a failure is theirs to see, not an unhandled
117
+ // rejection of the memo.
118
+ answer.catch(() => { });
119
+ this.answers.set(name, answer);
120
+ }
121
+ return answer;
122
+ }
123
+ }
124
+ exports.StoreReads = StoreReads;
125
+ /**
126
+ * Properties a promise or a serialiser looks up on any object — awaiting an
127
+ * answer reads its `then` — which are no field a build reads: never noted.
128
+ */
129
+ const NOT_FIELDS = new Set(['then', 'toJSON']);
130
+ /**
131
+ * A store answer as the broker uses it: a fresh, frozen plain object holding
132
+ * the contract's fields of the source, each read through the answer by plain
133
+ * property access — own or inherited, a field or a getter, whatever the
134
+ * answer's prototype — and copied deeply: arrays element by element, nested
135
+ * objects field by field. The store's own object is never handed on, so
136
+ * nothing the store does to it later reaches a provider built from it. A
137
+ * field of a shape the broker cannot take is refused naming it.
138
+ */
139
+ function snapshot(name, answer, refuse) {
140
+ if (answer === null || answer === undefined)
141
+ return null;
142
+ if (typeof answer !== 'object')
143
+ throw refuse(name);
144
+ const copy = {};
145
+ for (const field of CONTRACT_FIELDS[name]) {
146
+ let value;
147
+ try {
148
+ value = answer[field];
149
+ }
150
+ catch {
151
+ throw refuse(field);
152
+ }
153
+ if (value === undefined)
154
+ continue;
155
+ copy[field] = snapshotValue(value, field, refuse, new Set());
156
+ }
157
+ return Object.freeze(copy);
158
+ }
159
+ /** One value of a field, copied deeply and frozen; refused when it cannot be. */
160
+ function snapshotValue(value, field, refuse, seen) {
161
+ if (value === null)
162
+ return null;
163
+ switch (typeof value) {
164
+ case 'string':
165
+ case 'number':
166
+ case 'boolean':
167
+ case 'bigint':
168
+ case 'undefined':
169
+ return value;
170
+ case 'object':
171
+ break;
172
+ default:
173
+ // A function or a symbol.
174
+ throw refuse(field);
175
+ }
176
+ const object = value;
177
+ if (seen.has(object))
178
+ throw refuse(field);
179
+ seen.add(object);
180
+ try {
181
+ if (Array.isArray(object)) {
182
+ const length = object.length;
183
+ const copy = [];
184
+ for (let i = 0; i < length; i += 1) {
185
+ copy.push(snapshotValue(object[i], field, refuse, seen));
186
+ }
187
+ return Object.freeze(copy);
188
+ }
189
+ const copy = {};
190
+ for (const key of Object.keys(object)) {
191
+ copy[key] = snapshotValue(object[key], field, refuse, seen);
192
+ }
193
+ return Object.freeze(copy);
194
+ }
195
+ catch {
196
+ // A getter or a proxy that threw inside the value, or a part of it of a
197
+ // shape the broker cannot take: the field's fault, whatever was thrown.
198
+ throw refuse(field);
199
+ }
200
+ finally {
201
+ seen.delete(object);
202
+ }
203
+ }
204
+ /** A copy of a value as read: arrays and plain objects copied, element by element. */
205
+ function copied(value) {
206
+ if (Array.isArray(value))
207
+ return value.map(copied);
208
+ if (isPlainObject(value)) {
209
+ const copy = {};
210
+ for (const key of Object.keys(value))
211
+ copy[key] = copied(value[key]);
212
+ return copy;
213
+ }
214
+ return value;
215
+ }
216
+ function isPlainObject(value) {
217
+ if (value === null || typeof value !== 'object')
218
+ return false;
219
+ const prototype = Object.getPrototypeOf(value);
220
+ return prototype === Object.prototype || prototype === null;
221
+ }
222
+ /**
223
+ * Exact equality of two values as read: `Object.is` for everything but arrays
224
+ * and plain objects, which are equal element by element (and key by key).
225
+ */
226
+ function sameValue(a, b) {
227
+ if (Array.isArray(a) || Array.isArray(b)) {
228
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
229
+ return false;
230
+ }
231
+ return a.every((element, i) => sameValue(element, b[i]));
232
+ }
233
+ if (isPlainObject(a) && isPlainObject(b)) {
234
+ const keys = Object.keys(a).sort();
235
+ const others = Object.keys(b).sort();
236
+ if (!sameValue(keys, others))
237
+ return false;
238
+ return keys.every((key) => sameValue(a[key], b[key]));
239
+ }
240
+ return Object.is(a, b);
241
+ }
242
+ /** The own string keys of an answer, in a fixed order. */
243
+ function keysOf(target) {
244
+ return Object.keys(target).sort();
245
+ }
246
+ /**
247
+ * The one recording accessor of a build: every source the build reads goes
248
+ * through `read`, and every field read of what it answers — a property read,
249
+ * an `in` test, an enumeration — is noted, once, as given. `seal()` ends the
250
+ * recording: what is read afterwards is not the build's.
251
+ */
252
+ class IdentityRecorder {
253
+ reads;
254
+ entries = [];
255
+ noted = new Set();
256
+ sealed = false;
257
+ /** @param reads The call's reads, which every recorded source goes through. */
258
+ constructor(reads) {
259
+ this.reads = reads;
260
+ }
261
+ /**
262
+ * The source's answer, behind the recording proxy, or `null`. A read that
263
+ * fails is noted as failed and rethrown.
264
+ */
265
+ async read(name) {
266
+ let answer;
267
+ try {
268
+ answer = await this.reads.read(name);
269
+ }
270
+ catch (error) {
271
+ this.note(`${name}`, { source: name, kind: 'answer', value: 'failed' });
272
+ throw error;
273
+ }
274
+ this.note(`${name}`, {
275
+ source: name,
276
+ kind: 'answer',
277
+ value: answer ? 'some' : 'none',
278
+ });
279
+ return answer ? this.watched(name, answer) : null;
280
+ }
281
+ /**
282
+ * Notes a value the build derived from sources it reads without the
283
+ * recording proxy, with how to derive it again from a later call's reads.
284
+ * For sources whose answers change for reasons that are no change of the
285
+ * means — the session store's, which the broker's own writes change: the
286
+ * identity is what the build took from them, never what else they hold.
287
+ */
288
+ derived(value, derive) {
289
+ if (this.sealed)
290
+ return;
291
+ this.entries.push({ kind: 'derived', value: copied(value), derive });
292
+ }
293
+ /** The identity as read so far; nothing read later is recorded. */
294
+ seal() {
295
+ this.sealed = true;
296
+ return new BuildIdentity([...this.entries]);
297
+ }
298
+ note(key, entry) {
299
+ if (this.sealed || this.noted.has(key))
300
+ return;
301
+ this.noted.add(key);
302
+ this.entries.push(entry);
303
+ }
304
+ watched(source, target) {
305
+ return new Proxy(target, {
306
+ get: (object, property) => {
307
+ const value = Reflect.get(object, property);
308
+ if (typeof property === 'string' && !NOT_FIELDS.has(property)) {
309
+ this.note(`${source}.get.${property}`, {
310
+ source,
311
+ kind: 'get',
312
+ field: property,
313
+ value: copied(value),
314
+ });
315
+ }
316
+ return value;
317
+ },
318
+ has: (object, property) => {
319
+ const value = Reflect.has(object, property);
320
+ if (typeof property === 'string' && !NOT_FIELDS.has(property)) {
321
+ this.note(`${source}.has.${property}`, {
322
+ source,
323
+ kind: 'has',
324
+ field: property,
325
+ value,
326
+ });
327
+ }
328
+ return value;
329
+ },
330
+ ownKeys: (object) => {
331
+ this.note(`${source}.keys`, {
332
+ source,
333
+ kind: 'keys',
334
+ value: keysOf(object),
335
+ });
336
+ return Reflect.ownKeys(object);
337
+ },
338
+ });
339
+ }
340
+ }
341
+ exports.IdentityRecorder = IdentityRecorder;
342
+ /**
343
+ * What one build read, sealed: compared with a fresh read of the same
344
+ * sources on every call. Holds secrets the build read, in memory only.
345
+ */
346
+ class BuildIdentity {
347
+ entries;
348
+ constructor(entries) {
349
+ this.entries = entries;
350
+ }
351
+ /**
352
+ * Whether every fact this build read is still what the stores answer —
353
+ * each source re-read through `reads` (once per call), each field compared
354
+ * exactly. A source that cannot be read now, where it could then, is a
355
+ * change: the new build meets the failure itself.
356
+ */
357
+ async unchanged(reads) {
358
+ const answers = new Map();
359
+ for (const entry of this.entries) {
360
+ if (entry.kind === 'derived') {
361
+ const now = await entry.derive(reads).then((value) => ({ value }), () => undefined);
362
+ if (!now || !sameValue(entry.value, now.value))
363
+ return false;
364
+ continue;
365
+ }
366
+ let answer = answers.get(entry.source);
367
+ if (answer === undefined) {
368
+ answer = await reads.read(entry.source).then((value) => value, () => 'failed');
369
+ answers.set(entry.source, answer);
370
+ }
371
+ if (!sameFact(entry, answer))
372
+ return false;
373
+ }
374
+ return true;
375
+ }
376
+ }
377
+ exports.BuildIdentity = BuildIdentity;
378
+ function sameFact(entry, answer) {
379
+ if (entry.kind === 'answer') {
380
+ const now = answer === 'failed' ? 'failed' : answer ? 'some' : 'none';
381
+ return entry.value === now;
382
+ }
383
+ if (answer === null || answer === 'failed')
384
+ return false;
385
+ switch (entry.kind) {
386
+ case 'get':
387
+ return sameValue(entry.value, Reflect.get(answer, entry.field));
388
+ case 'has':
389
+ return entry.value === Reflect.has(answer, entry.field);
390
+ case 'keys':
391
+ return sameValue(entry.value, keysOf(answer));
392
+ }
393
+ }