@qlzt/dsh-api-remote-access 0.0.0-stage → 0.2.1-rc.2
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/LICENSE +21 -0
- package/README.i18n.yaml +34 -0
- package/README.md +96 -2
- package/README.zh.md +97 -0
- package/lib/client.js +733 -0
- package/lib/index.js +441 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +233 -0
- package/lib/typert.remote-client.d.ts +26 -0
- package/lib/typert.remote-client.js +75 -0
- package/lib/types/client/index.d.ts +22 -0
- package/lib/types/client/index.js +25 -0
- package/lib/types/client/mount.d.ts +50 -0
- package/lib/types/client/mount.js +81 -0
- package/lib/types/client/state.d.ts +63 -0
- package/lib/types/client/state.js +182 -0
- package/lib/types/index.d.ts +121 -0
- package/lib/types/index.js +405 -0
- package/lib/types/snapshot.d.ts +8 -0
- package/lib/types/snapshot.js +8 -0
- package/lib/types/types.d.ts +130 -0
- package/lib/types/types.js +7 -0
- package/package.json +81 -4
package/lib/index.js
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { Remote, TypertRemoteService, principalOf } from "@qlzt/dsh-typert-protocol";
|
|
2
|
+
//#region lib/types/snapshot.js
|
|
3
|
+
/**
|
|
4
|
+
* The index global carrying a page's first access snapshot, shared by the
|
|
5
|
+
* Host row that writes it and the Client mount that reads it.
|
|
6
|
+
* @module @qlzt/dsh-api-remote-access/src/snapshot
|
|
7
|
+
*/
|
|
8
|
+
/** `globalThis` property an index injection row assigns to the page's opening baseline. */
|
|
9
|
+
const REMOTE_ACCESS_SNAPSHOT_KEY = "__DSH_REMOTE_ACCESS__";
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region lib/types/index.js
|
|
12
|
+
/**
|
|
13
|
+
* Host method-level access control. The Gateway asks `decide()` before it
|
|
14
|
+
* dispatches a non-owner call or forwards an event to a non-owner Peer; list
|
|
15
|
+
* methods filter items through `allows()`; every Peer reads its own access
|
|
16
|
+
* snapshot, first from the index and then from `follow()`. Business packages
|
|
17
|
+
* register one policy per non-owner principal kind.
|
|
18
|
+
* @module @qlzt/dsh-api-remote-access
|
|
19
|
+
*/
|
|
20
|
+
var __runInitializers = function(thisArg, initializers, value) {
|
|
21
|
+
var useValue = arguments.length > 2;
|
|
22
|
+
for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
23
|
+
return useValue ? value : void 0;
|
|
24
|
+
};
|
|
25
|
+
var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
26
|
+
function accept(f) {
|
|
27
|
+
if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
|
|
28
|
+
return f;
|
|
29
|
+
}
|
|
30
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
31
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
32
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
33
|
+
var _, done = false;
|
|
34
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
35
|
+
var context = {};
|
|
36
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
37
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
38
|
+
context.addInitializer = function(f) {
|
|
39
|
+
if (done) throw new TypeError("Cannot add initializers after decoration has completed");
|
|
40
|
+
extraInitializers.push(accept(f || null));
|
|
41
|
+
};
|
|
42
|
+
var result = (0, decorators[i])(kind === "accessor" ? {
|
|
43
|
+
get: descriptor.get,
|
|
44
|
+
set: descriptor.set
|
|
45
|
+
} : descriptor[key], context);
|
|
46
|
+
if (kind === "accessor") {
|
|
47
|
+
if (result === void 0) continue;
|
|
48
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
49
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
50
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
51
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
52
|
+
} else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
|
|
53
|
+
else descriptor[key] = _;
|
|
54
|
+
}
|
|
55
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
56
|
+
done = true;
|
|
57
|
+
};
|
|
58
|
+
/** The snapshot endpoint every Peer may read for itself. */
|
|
59
|
+
const FOLLOW_ACTION = "remoteAccess/follow";
|
|
60
|
+
const ALLOW = Object.freeze({ kind: "allow" });
|
|
61
|
+
const NO_ALLOW = Object.freeze({
|
|
62
|
+
kind: "deny",
|
|
63
|
+
reason: "no-allow"
|
|
64
|
+
});
|
|
65
|
+
const POLICY_FAILED = Object.freeze({
|
|
66
|
+
kind: "deny",
|
|
67
|
+
reason: "policy-failed"
|
|
68
|
+
});
|
|
69
|
+
const OWNER = Object.freeze({ kind: "owner" });
|
|
70
|
+
const OWNER_BASELINE = Object.freeze({
|
|
71
|
+
type: "baseline",
|
|
72
|
+
owner: true
|
|
73
|
+
});
|
|
74
|
+
const ALLOWED = Object.freeze({ kind: "allow" });
|
|
75
|
+
const FAILED = Object.freeze({ kind: "failed" });
|
|
76
|
+
/** One open non-owner snapshot generation; changes coalesce until its reader wakes. */
|
|
77
|
+
var AccessFollower = class {
|
|
78
|
+
principal;
|
|
79
|
+
dirty = false;
|
|
80
|
+
wake;
|
|
81
|
+
/** @param principal - the generation's caller identity; never the owner. */
|
|
82
|
+
constructor(principal) {
|
|
83
|
+
this.principal = principal;
|
|
84
|
+
}
|
|
85
|
+
/** Mark the generation stale and wake its reader. */
|
|
86
|
+
touch() {
|
|
87
|
+
this.dirty = true;
|
|
88
|
+
this.wake?.();
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Wait until the generation is stale or cancelled.
|
|
92
|
+
* @param signal - generation cancellation.
|
|
93
|
+
* @returns true when stale; false once cancelled.
|
|
94
|
+
*/
|
|
95
|
+
async changed(signal) {
|
|
96
|
+
const abort = () => {
|
|
97
|
+
this.wake?.();
|
|
98
|
+
};
|
|
99
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
100
|
+
try {
|
|
101
|
+
while (!this.dirty && !signal.aborted) {
|
|
102
|
+
await new Promise((resolve) => {
|
|
103
|
+
this.wake = resolve;
|
|
104
|
+
});
|
|
105
|
+
this.wake = void 0;
|
|
106
|
+
}
|
|
107
|
+
} finally {
|
|
108
|
+
signal.removeEventListener("abort", abort);
|
|
109
|
+
}
|
|
110
|
+
this.dirty = false;
|
|
111
|
+
return !signal.aborted;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Registry of access policies, the Gateway's access decisions, list
|
|
116
|
+
* filtering, and the per-Peer access snapshot. Every composition that mounts
|
|
117
|
+
* the Gateway mounts this service too.
|
|
118
|
+
*/
|
|
119
|
+
let RemoteAccessService = (() => {
|
|
120
|
+
let _classSuper = TypertRemoteService;
|
|
121
|
+
let _instanceExtraInitializers = [];
|
|
122
|
+
let _follow_decorators;
|
|
123
|
+
return class RemoteAccessService extends _classSuper {
|
|
124
|
+
static {
|
|
125
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
126
|
+
_follow_decorators = [Remote({ mode: "stream" })];
|
|
127
|
+
__esDecorate(this, null, _follow_decorators, {
|
|
128
|
+
kind: "method",
|
|
129
|
+
name: "follow",
|
|
130
|
+
static: false,
|
|
131
|
+
private: false,
|
|
132
|
+
access: {
|
|
133
|
+
has: (obj) => "follow" in obj,
|
|
134
|
+
get: (obj) => obj.follow
|
|
135
|
+
},
|
|
136
|
+
metadata: _metadata
|
|
137
|
+
}, null, _instanceExtraInitializers);
|
|
138
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, {
|
|
139
|
+
enumerable: true,
|
|
140
|
+
configurable: true,
|
|
141
|
+
writable: true,
|
|
142
|
+
value: _metadata
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
policies = (__runInitializers(this, _instanceExtraInitializers), /* @__PURE__ */ new Map());
|
|
146
|
+
/** Policies whose failure was logged since the last `invalidate()`; their later failures stay silent. */
|
|
147
|
+
reported = /* @__PURE__ */ new Set();
|
|
148
|
+
followers = /* @__PURE__ */ new Set();
|
|
149
|
+
/** Aborted when this instance unloads; every open snapshot generation ends with it. */
|
|
150
|
+
lifetime = new AbortController();
|
|
151
|
+
/** @param ctx - owning Host plugin Context. */
|
|
152
|
+
constructor(ctx) {
|
|
153
|
+
super(ctx, "remoteAccess");
|
|
154
|
+
ctx.effect(() => () => {
|
|
155
|
+
this.lifetime.abort();
|
|
156
|
+
}, "remoteAccess: snapshot generations");
|
|
157
|
+
ctx.inject(["webServer"], (webCtx) => {
|
|
158
|
+
webCtx.on("webserver/index-inject", (table) => {
|
|
159
|
+
table.push({
|
|
160
|
+
kind: "global",
|
|
161
|
+
name: REMOTE_ACCESS_SNAPSHOT_KEY,
|
|
162
|
+
value: OWNER_BASELINE
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Register the policy of one non-owner principal kind in the caller's
|
|
169
|
+
* fiber; open snapshots are recomputed on registration and removal.
|
|
170
|
+
* @param kind - the principal kind the policy serves; never `owner`.
|
|
171
|
+
* @param policy - business-owned bindings and refusal reasons for principals of `kind`.
|
|
172
|
+
* @returns disposer removing this policy.
|
|
173
|
+
* @throws Error when `kind` already has a policy.
|
|
174
|
+
*/
|
|
175
|
+
registerPolicy(kind, policy) {
|
|
176
|
+
const policies = this.policies;
|
|
177
|
+
if (policies.has(kind)) throw new Error(`remote access: principal kind ${JSON.stringify(kind)} already has a policy`);
|
|
178
|
+
const entry = {
|
|
179
|
+
kind,
|
|
180
|
+
policy
|
|
181
|
+
};
|
|
182
|
+
const dispose = this.ctx.effect(() => {
|
|
183
|
+
policies.set(kind, entry);
|
|
184
|
+
this.invalidate();
|
|
185
|
+
return () => {
|
|
186
|
+
policies.delete(kind);
|
|
187
|
+
this.invalidate();
|
|
188
|
+
};
|
|
189
|
+
}, `remoteAccess.registerPolicy(${JSON.stringify(kind)})`);
|
|
190
|
+
return () => {
|
|
191
|
+
dispose();
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Report that the bindings or condition answers for `principal` may have
|
|
196
|
+
* changed. Business packages call this on every grant, revocation, mode
|
|
197
|
+
* change, and expiry; open snapshots re-push, and the next failure of each
|
|
198
|
+
* policy is logged again.
|
|
199
|
+
* @param principal - affected principal; omitted for every principal.
|
|
200
|
+
*/
|
|
201
|
+
invalidate(principal) {
|
|
202
|
+
this.reported.clear();
|
|
203
|
+
const key = principal === void 0 ? void 0 : canonicalJson(principal);
|
|
204
|
+
for (const follower of this.followers) if (key === void 0 || canonicalJson(follower.principal) === key) follower.touch();
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Gateway-only: decide one request whose resources the Gateway resolved.
|
|
208
|
+
* The owner is allowed, and so is `remoteAccess/follow`: every Peer reads
|
|
209
|
+
* its own snapshot. Otherwise the action is allowed only when, on every
|
|
210
|
+
* listed resource, some binding of the principal's policy names that
|
|
211
|
+
* resource, has a role listing the action, and has no condition or a
|
|
212
|
+
* condition that allows. `bindings()` is called once per decision.
|
|
213
|
+
* @param request - the caller or recipient, the action, the resources it
|
|
214
|
+
* touches, and a call's arguments.
|
|
215
|
+
* @returns `allow`; or `deny` with `no-allow` when the principal kind has
|
|
216
|
+
* no policy, the first refusing condition's reason on the first resource
|
|
217
|
+
* no binding allows, else `explain()`'s reason, else `no-allow`;
|
|
218
|
+
* `policy-failed` when `bindings()`, a condition, or `explain()` throws,
|
|
219
|
+
* logged once per policy until the next `invalidate()`. The Gateway
|
|
220
|
+
* throws `remote-access/denied` for a denied call and drops a denied event.
|
|
221
|
+
*/
|
|
222
|
+
decide(request) {
|
|
223
|
+
const { principal, action } = request;
|
|
224
|
+
if (principal.kind === "owner" || action === FOLLOW_ACTION) return ALLOW;
|
|
225
|
+
const entry = this.policies.get(principal.kind);
|
|
226
|
+
if (entry === void 0) return NO_ALLOW;
|
|
227
|
+
const bindings = this.bindingsOf(entry, principal, action);
|
|
228
|
+
if (bindings === void 0) return POLICY_FAILED;
|
|
229
|
+
for (const resource of request.resources) {
|
|
230
|
+
const evaluation = this.evaluate(entry, bindings, action, resource, request.args);
|
|
231
|
+
switch (evaluation.kind) {
|
|
232
|
+
case "allow": continue;
|
|
233
|
+
case "failed": return POLICY_FAILED;
|
|
234
|
+
case "refuse": return evaluation.refusal ?? this.explain(entry, request);
|
|
235
|
+
/* v8 ignore next -- closed union. */
|
|
236
|
+
default: return assertNever(evaluation);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return ALLOW;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Filter items inside a Remote method. The invocation is read from the
|
|
243
|
+
* Context that accessed this service, so call it as
|
|
244
|
+
* `this.ctx.remoteAccess.allows(resource)` from the Remote method. Each
|
|
245
|
+
* check reads the policy's bindings again, so a long-running stream sees
|
|
246
|
+
* binding changes from the next check on.
|
|
247
|
+
* @param resource - resource of one result item.
|
|
248
|
+
* @returns true for the owner and outside a Remote call; otherwise whether
|
|
249
|
+
* the invocation's endpoint action is allowed on `resource` without call
|
|
250
|
+
* arguments, false when the policy is absent or fails.
|
|
251
|
+
*/
|
|
252
|
+
allows(resource) {
|
|
253
|
+
const invocation = this.ctx.invocation;
|
|
254
|
+
const principal = principalOf(invocation);
|
|
255
|
+
if (invocation === void 0 || principal === void 0) return true;
|
|
256
|
+
const action = `${invocation.request.namespace}/${invocation.request.method}`;
|
|
257
|
+
const entry = this.policies.get(principal.kind);
|
|
258
|
+
if (entry === void 0) return false;
|
|
259
|
+
const bindings = this.bindingsOf(entry, principal, action);
|
|
260
|
+
return bindings !== void 0 && this.evaluate(entry, bindings, action, resource, void 0).kind === "allow";
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The baseline a page opens with before its snapshot stream connects. A
|
|
264
|
+
* business package rendering a non-owner page assigns this value to
|
|
265
|
+
* {@link REMOTE_ACCESS_SNAPSHOT_KEY} after every index injection row,
|
|
266
|
+
* replacing the owner baseline the webServer index carries.
|
|
267
|
+
* @param principal - the page's caller identity.
|
|
268
|
+
* @returns the owner baseline for the owner; otherwise the principal's current grants.
|
|
269
|
+
*/
|
|
270
|
+
snapshotFor(principal) {
|
|
271
|
+
if (principal.kind === "owner") return OWNER_BASELINE;
|
|
272
|
+
return {
|
|
273
|
+
type: "baseline",
|
|
274
|
+
owner: false,
|
|
275
|
+
grants: this.grantsFor(principal)
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Stream the calling Peer's access snapshot: one baseline, then `upsert`
|
|
280
|
+
* and `remove` frames after each `invalidate()` and each policy
|
|
281
|
+
* registration or removal that changes a grant. The owner receives only
|
|
282
|
+
* `{ owner: true }`. The generation ends when `signal` aborts or this
|
|
283
|
+
* service unloads, so a reloaded service starts every reader from a fresh
|
|
284
|
+
* baseline.
|
|
285
|
+
* @param signal - generation cancellation.
|
|
286
|
+
* @returns the access frames of one generation.
|
|
287
|
+
*/
|
|
288
|
+
async *follow(signal) {
|
|
289
|
+
signal.throwIfAborted();
|
|
290
|
+
const generation = AbortSignal.any([signal, this.lifetime.signal]);
|
|
291
|
+
const principal = principalOf(this.ctx.invocation);
|
|
292
|
+
if (principal === void 0) {
|
|
293
|
+
yield this.snapshotFor(OWNER);
|
|
294
|
+
await abortion(generation);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const follower = new AccessFollower(principal);
|
|
298
|
+
this.followers.add(follower);
|
|
299
|
+
try {
|
|
300
|
+
let sent = this.grantsFor(principal);
|
|
301
|
+
yield {
|
|
302
|
+
type: "baseline",
|
|
303
|
+
owner: false,
|
|
304
|
+
grants: sent
|
|
305
|
+
};
|
|
306
|
+
while (await follower.changed(generation)) {
|
|
307
|
+
const next = this.grantsFor(principal);
|
|
308
|
+
const previous = sent;
|
|
309
|
+
sent = next;
|
|
310
|
+
for (const grant of next) {
|
|
311
|
+
const before = previous.find((other) => sameResource(other.resource, grant.resource));
|
|
312
|
+
if (before === void 0 || !sameActions(before.actions, grant.actions)) yield {
|
|
313
|
+
type: "upsert",
|
|
314
|
+
grant
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
for (const grant of previous) if (!next.some((other) => sameResource(other.resource, grant.resource))) yield {
|
|
318
|
+
type: "remove",
|
|
319
|
+
resource: grant.resource
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
} finally {
|
|
323
|
+
this.followers.delete(follower);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/** One `bindings()` call; `undefined` after a logged failure. */
|
|
327
|
+
bindingsOf(entry, principal, purpose) {
|
|
328
|
+
try {
|
|
329
|
+
return entry.policy.bindings(principal);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
this.report(entry, `list bindings for ${purpose}`, error);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/** Find a binding on `resource` whose role lists `action` and whose condition, if any, allows. */
|
|
336
|
+
evaluate(entry, bindings, action, resource, args) {
|
|
337
|
+
let refusal;
|
|
338
|
+
for (const binding of bindings) {
|
|
339
|
+
if (!sameResource(binding.resource, resource) || !binding.role.actions.includes(action)) continue;
|
|
340
|
+
if (binding.condition === void 0) return ALLOWED;
|
|
341
|
+
let decision;
|
|
342
|
+
try {
|
|
343
|
+
decision = binding.condition(args === void 0 ? {
|
|
344
|
+
action,
|
|
345
|
+
resource
|
|
346
|
+
} : {
|
|
347
|
+
action,
|
|
348
|
+
resource,
|
|
349
|
+
args
|
|
350
|
+
});
|
|
351
|
+
} catch (error) {
|
|
352
|
+
this.report(entry, `evaluate a condition for ${action}`, error);
|
|
353
|
+
return FAILED;
|
|
354
|
+
}
|
|
355
|
+
if (decision.kind === "allow") return ALLOWED;
|
|
356
|
+
refusal ??= decision;
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
kind: "refuse",
|
|
360
|
+
refusal
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
/** The refusal reason `explain()` names, else `no-allow`; `policy-failed` when it throws. */
|
|
364
|
+
explain(entry, request) {
|
|
365
|
+
let reason;
|
|
366
|
+
try {
|
|
367
|
+
reason = entry.policy.explain?.(request);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
this.report(entry, `explain a refusal of ${request.action}`, error);
|
|
370
|
+
return POLICY_FAILED;
|
|
371
|
+
}
|
|
372
|
+
return reason === void 0 ? NO_ALLOW : {
|
|
373
|
+
kind: "deny",
|
|
374
|
+
reason
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* One grant per bound resource, in first-binding order: the endpoint
|
|
379
|
+
* actions of the resource's bindings that `decide()` would allow there
|
|
380
|
+
* without call arguments. A resource with no such action gets no grant;
|
|
381
|
+
* event actions stay out of the snapshot.
|
|
382
|
+
*/
|
|
383
|
+
grantsFor(principal) {
|
|
384
|
+
const entry = this.policies.get(principal.kind);
|
|
385
|
+
if (entry === void 0) return [];
|
|
386
|
+
const bindings = this.bindingsOf(entry, principal, "a snapshot");
|
|
387
|
+
if (bindings === void 0) return [];
|
|
388
|
+
const grants = [];
|
|
389
|
+
const seen = [];
|
|
390
|
+
for (const { resource } of bindings) {
|
|
391
|
+
if (seen.some((other) => sameResource(other, resource))) continue;
|
|
392
|
+
seen.push(resource);
|
|
393
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
394
|
+
for (const binding of bindings) {
|
|
395
|
+
if (!sameResource(binding.resource, resource)) continue;
|
|
396
|
+
for (const action of binding.role.actions) if (!action.startsWith("event:")) candidates.add(action);
|
|
397
|
+
}
|
|
398
|
+
const actions = [...candidates].filter((action) => this.evaluate(entry, bindings, action, resource, void 0).kind === "allow").sort();
|
|
399
|
+
if (actions.length > 0) grants.push(Object.freeze({
|
|
400
|
+
resource,
|
|
401
|
+
actions: Object.freeze(actions)
|
|
402
|
+
}));
|
|
403
|
+
}
|
|
404
|
+
return grants;
|
|
405
|
+
}
|
|
406
|
+
/** Log a policy failure once per policy until the next `invalidate()`; snapshots and lists ask many times per change. */
|
|
407
|
+
report(entry, action, error) {
|
|
408
|
+
if (this.reported.has(entry)) return;
|
|
409
|
+
this.reported.add(entry);
|
|
410
|
+
this.ctx.logger.warn(`remote access: the ${JSON.stringify(entry.kind)} policy failed to ${action}; its further failures are not logged until access state changes`);
|
|
411
|
+
this.ctx.logger.warn(error);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
})();
|
|
415
|
+
/* v8 ignore next 3 -- closed-union backstop. */
|
|
416
|
+
function assertNever(value) {
|
|
417
|
+
throw new Error(`remote access: unreachable value ${JSON.stringify(value)}`);
|
|
418
|
+
}
|
|
419
|
+
/** Key-order-independent JSON text identifying one principal. */
|
|
420
|
+
function canonicalJson(value) {
|
|
421
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
422
|
+
if (typeof value === "object" && value !== null) return `{${Object.entries(value).sort(([left], [right]) => left < right ? -1 : 1).map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
|
|
423
|
+
return JSON.stringify(value);
|
|
424
|
+
}
|
|
425
|
+
/** Resources are equal when their kinds and ids are; `global` carries no id. */
|
|
426
|
+
function sameResource(left, right) {
|
|
427
|
+
return left.kind === right.kind && left.id === right.id;
|
|
428
|
+
}
|
|
429
|
+
function sameActions(left, right) {
|
|
430
|
+
return left.length === right.length && left.every((action, index) => action === right[index]);
|
|
431
|
+
}
|
|
432
|
+
function abortion(signal) {
|
|
433
|
+
return new Promise((resolve) => {
|
|
434
|
+
if (signal.aborted) resolve();
|
|
435
|
+
else signal.addEventListener("abort", () => {
|
|
436
|
+
resolve();
|
|
437
|
+
}, { once: true });
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
//#endregion
|
|
441
|
+
export { REMOTE_ACCESS_SNAPSHOT_KEY, RemoteAccessService, RemoteAccessService as default };
|