@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/CHANGELOG.md +273 -1
- package/README.md +1028 -908
- package/dist/AuthBroker.d.ts +353 -99
- package/dist/AuthBroker.d.ts.map +1 -1
- package/dist/AuthBroker.js +806 -258
- package/dist/DestinationConfigError.d.ts +35 -6
- package/dist/DestinationConfigError.d.ts.map +1 -1
- package/dist/DestinationConfigError.js +51 -9
- package/dist/SessionWriter.d.ts +70 -33
- package/dist/SessionWriter.d.ts.map +1 -1
- package/dist/SessionWriter.js +118 -94
- package/dist/binding.d.ts +137 -64
- package/dist/binding.d.ts.map +1 -1
- package/dist/binding.js +330 -131
- package/dist/bindingOf.d.ts +33 -14
- package/dist/bindingOf.d.ts.map +1 -1
- package/dist/bindingOf.js +50 -20
- package/dist/buildIdentity.d.ts +137 -0
- package/dist/buildIdentity.d.ts.map +1 -0
- package/dist/buildIdentity.js +393 -0
- package/dist/clientAuthentication.d.ts +124 -0
- package/dist/clientAuthentication.d.ts.map +1 -0
- package/dist/clientAuthentication.js +218 -0
- package/dist/contractShape.d.ts +24 -0
- package/dist/contractShape.d.ts.map +1 -0
- package/dist/contractShape.js +24 -0
- package/dist/destinations.d.ts +174 -53
- package/dist/destinations.d.ts.map +1 -1
- package/dist/destinations.js +460 -150
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/quietLogger.d.ts +24 -0
- package/dist/quietLogger.d.ts.map +1 -0
- package/dist/quietLogger.js +63 -0
- package/package.json +9 -7
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
|
|
15
|
-
*
|
|
16
|
-
* the
|
|
17
|
-
*
|
|
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
|
-
|
|
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
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* `getProvider`
|
|
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
|
|
38
|
-
* destination has one. A destination that states no
|
|
39
|
-
*
|
|
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 (
|
|
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
|
+
}
|