@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
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React-free Client access state: the latest `remoteAccess/follow` snapshot
|
|
3
|
+
* and identity-stable boolean sources per resource and action.
|
|
4
|
+
* @module @qlzt/dsh-api-remote-access/src/client/state
|
|
5
|
+
*/
|
|
6
|
+
const CLOSED = Object.freeze({ kind: 'closed' });
|
|
7
|
+
const OWNER_SNAPSHOT = Object.freeze({ kind: 'owner' });
|
|
8
|
+
const GLOBAL_KEY = '\u0000global';
|
|
9
|
+
/**
|
|
10
|
+
* Validate the opening baseline an index injection row assigned to the page.
|
|
11
|
+
* @param value - the injected global; `undefined` when the page carries none.
|
|
12
|
+
* @returns the baseline, or `undefined` when none was injected.
|
|
13
|
+
* @throws TypeError when the injected value is not a baseline frame.
|
|
14
|
+
*/
|
|
15
|
+
export function parseInjectedBaseline(value) {
|
|
16
|
+
if (value === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
if (!isRecord(value) || value['type'] !== 'baseline')
|
|
19
|
+
throw new TypeError('remote access: the injected access snapshot is not a baseline frame');
|
|
20
|
+
if (value['owner'] === true)
|
|
21
|
+
return { type: 'baseline', owner: true };
|
|
22
|
+
const grants = value['grants'];
|
|
23
|
+
if (value['owner'] !== false || !Array.isArray(grants) || !grants.every(isGrant)) {
|
|
24
|
+
throw new TypeError('remote access: the injected access snapshot carries malformed grants');
|
|
25
|
+
}
|
|
26
|
+
return { type: 'baseline', owner: false, grants };
|
|
27
|
+
}
|
|
28
|
+
function isGrant(value) {
|
|
29
|
+
if (!isRecord(value) || !isResource(value['resource']))
|
|
30
|
+
return false;
|
|
31
|
+
const actions = value['actions'];
|
|
32
|
+
return Array.isArray(actions) && actions.every(action => typeof action === 'string');
|
|
33
|
+
}
|
|
34
|
+
function isResource(value) {
|
|
35
|
+
if (!isRecord(value) || typeof value['kind'] !== 'string')
|
|
36
|
+
return false;
|
|
37
|
+
return value['kind'] === 'global' || typeof value['id'] === 'string';
|
|
38
|
+
}
|
|
39
|
+
function isRecord(value) {
|
|
40
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Snapshot owner for one Client generation. Sources are cached per
|
|
44
|
+
* `(kind, id, action)` so Hook bindings keep their identity; each source
|
|
45
|
+
* computes its boolean from the current snapshot.
|
|
46
|
+
*/
|
|
47
|
+
export class ClientAccessState {
|
|
48
|
+
snapshot = CLOSED;
|
|
49
|
+
/** @param initial - the page's injected opening baseline; without one every answer is false until a baseline arrives. */
|
|
50
|
+
constructor(initial) {
|
|
51
|
+
if (initial !== undefined)
|
|
52
|
+
this.replace(initial);
|
|
53
|
+
}
|
|
54
|
+
listeners = new Set();
|
|
55
|
+
sources = new Map();
|
|
56
|
+
/**
|
|
57
|
+
* Answer for one action on the global resource.
|
|
58
|
+
* @param action - endpoint action the caller may invoke.
|
|
59
|
+
* @returns identity-stable boolean source.
|
|
60
|
+
*/
|
|
61
|
+
global(action) {
|
|
62
|
+
return this.cached(GLOBAL_KEY, '', action);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Answer for one action on one resource of a declared kind.
|
|
66
|
+
* @param kind - declared resource kind.
|
|
67
|
+
* @param id - resource identity.
|
|
68
|
+
* @param action - endpoint action the caller may invoke.
|
|
69
|
+
* @returns identity-stable boolean source.
|
|
70
|
+
*/
|
|
71
|
+
resource(kind, id, action) {
|
|
72
|
+
return this.cached(kind, id, action);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Replace the snapshot from one generation's opening frame.
|
|
76
|
+
* @param frame - owner or non-owner baseline.
|
|
77
|
+
*/
|
|
78
|
+
replace(frame) {
|
|
79
|
+
if (frame.owner) {
|
|
80
|
+
this.publish(OWNER_SNAPSHOT);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
let global = new Set();
|
|
84
|
+
const resources = new Map();
|
|
85
|
+
// The baseline is unpublished, so its maps are filled in place.
|
|
86
|
+
for (const { resource, actions } of frame.grants) {
|
|
87
|
+
if (resource.kind === 'global') {
|
|
88
|
+
global = new Set(actions);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
let grants = resources.get(resource.kind);
|
|
92
|
+
if (grants === undefined) {
|
|
93
|
+
grants = new Map();
|
|
94
|
+
resources.set(resource.kind, grants);
|
|
95
|
+
}
|
|
96
|
+
grants.set(resource.id, new Set(actions));
|
|
97
|
+
}
|
|
98
|
+
this.publish({ kind: 'granted', global, resources });
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Apply one increment of the current generation.
|
|
102
|
+
* @param frame - `upsert` or `remove`.
|
|
103
|
+
*/
|
|
104
|
+
update(frame) {
|
|
105
|
+
const current = this.snapshot;
|
|
106
|
+
if (current.kind !== 'granted')
|
|
107
|
+
return;
|
|
108
|
+
switch (frame.type) {
|
|
109
|
+
case 'upsert':
|
|
110
|
+
this.publish(withGrant(current, frame.grant));
|
|
111
|
+
return;
|
|
112
|
+
case 'remove':
|
|
113
|
+
this.publish(withGrant(current, { resource: frame.resource, actions: [] }, true));
|
|
114
|
+
return;
|
|
115
|
+
/* v8 ignore next -- the generated Remote codec validates this closed union. */
|
|
116
|
+
default:
|
|
117
|
+
return assertNever(frame);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Whether the current snapshot is the owner's, whose answers never depend on the stream. */
|
|
121
|
+
get owner() {
|
|
122
|
+
return this.snapshot.kind === 'owner';
|
|
123
|
+
}
|
|
124
|
+
/** The stream failed terminally: the owner keeps every answer; a non-owner's answers become false. */
|
|
125
|
+
unavailable() {
|
|
126
|
+
if (this.snapshot.kind !== 'owner')
|
|
127
|
+
this.publish(CLOSED);
|
|
128
|
+
}
|
|
129
|
+
cached(kind, id, action) {
|
|
130
|
+
const key = `${kind}\u0000${id}\u0000${action}`;
|
|
131
|
+
let source = this.sources.get(key);
|
|
132
|
+
if (source === undefined) {
|
|
133
|
+
source = {
|
|
134
|
+
getSnapshot: () => answer(this.snapshot, kind, id, action),
|
|
135
|
+
subscribe: (listener) => {
|
|
136
|
+
this.listeners.add(listener);
|
|
137
|
+
return () => { this.listeners.delete(listener); };
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
this.sources.set(key, source);
|
|
141
|
+
}
|
|
142
|
+
return source;
|
|
143
|
+
}
|
|
144
|
+
publish(next) {
|
|
145
|
+
this.snapshot = next;
|
|
146
|
+
for (const listener of [...this.listeners])
|
|
147
|
+
listener();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function answer(snapshot, kind, id, action) {
|
|
151
|
+
switch (snapshot.kind) {
|
|
152
|
+
case 'closed':
|
|
153
|
+
return false;
|
|
154
|
+
case 'owner':
|
|
155
|
+
return true;
|
|
156
|
+
case 'granted': {
|
|
157
|
+
const allowed = kind === GLOBAL_KEY ? snapshot.global : snapshot.resources.get(kind)?.get(id);
|
|
158
|
+
return allowed?.has(action) ?? false;
|
|
159
|
+
}
|
|
160
|
+
/* v8 ignore next -- closed union. */
|
|
161
|
+
default:
|
|
162
|
+
return assertNever(snapshot);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function withGrant(snapshot, grant, remove = false) {
|
|
166
|
+
const resource = grant.resource;
|
|
167
|
+
if (resource.kind === 'global')
|
|
168
|
+
return { ...snapshot, global: new Set(grant.actions) };
|
|
169
|
+
const resources = new Map(snapshot.resources);
|
|
170
|
+
const grants = new Map(resources.get(resource.kind));
|
|
171
|
+
if (remove)
|
|
172
|
+
grants.delete(resource.id);
|
|
173
|
+
else
|
|
174
|
+
grants.set(resource.id, new Set(grant.actions));
|
|
175
|
+
resources.set(resource.kind, grants);
|
|
176
|
+
return { ...snapshot, resources };
|
|
177
|
+
}
|
|
178
|
+
/* v8 ignore next 3 -- closed-union backstop. */
|
|
179
|
+
function assertNever(value) {
|
|
180
|
+
throw new Error(`unreachable access frame: ${JSON.stringify(value)}`);
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host method-level access control. The Gateway asks `decide()` before it
|
|
3
|
+
* dispatches a non-owner call or forwards an event to a non-owner Peer; list
|
|
4
|
+
* methods filter items through `allows()`; every Peer reads its own access
|
|
5
|
+
* snapshot, first from the index and then from `follow()`. Business packages
|
|
6
|
+
* register one policy per non-owner principal kind.
|
|
7
|
+
* @module @qlzt/dsh-api-remote-access
|
|
8
|
+
*/
|
|
9
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
10
|
+
import { TypertRemoteService, type Principal, type PrincipalMap, type RemoteAccessResource } from '@qlzt/dsh-typert-protocol';
|
|
11
|
+
import type { RemoteAccessDecision, RemoteAccessFrame, RemoteAccessPolicy, RemoteAccessRequest } from './types.ts';
|
|
12
|
+
export type * from './types.ts';
|
|
13
|
+
export { REMOTE_ACCESS_SNAPSHOT_KEY } from './snapshot.ts';
|
|
14
|
+
/** The opening frame of a snapshot generation. */
|
|
15
|
+
export type RemoteAccessBaseline = Extract<RemoteAccessFrame, {
|
|
16
|
+
type: 'baseline';
|
|
17
|
+
}>;
|
|
18
|
+
declare module '@deepseek-ai/cordis' {
|
|
19
|
+
interface Context {
|
|
20
|
+
/** Host method-level access control for non-owner Peers. */
|
|
21
|
+
remoteAccess: RemoteAccessService;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Registry of access policies, the Gateway's access decisions, list
|
|
26
|
+
* filtering, and the per-Peer access snapshot. Every composition that mounts
|
|
27
|
+
* the Gateway mounts this service too.
|
|
28
|
+
*/
|
|
29
|
+
export declare class RemoteAccessService extends TypertRemoteService {
|
|
30
|
+
private readonly policies;
|
|
31
|
+
/** Policies whose failure was logged since the last `invalidate()`; their later failures stay silent. */
|
|
32
|
+
private readonly reported;
|
|
33
|
+
private readonly followers;
|
|
34
|
+
/** Aborted when this instance unloads; every open snapshot generation ends with it. */
|
|
35
|
+
private readonly lifetime;
|
|
36
|
+
/** @param ctx - owning Host plugin Context. */
|
|
37
|
+
constructor(ctx: Context);
|
|
38
|
+
/**
|
|
39
|
+
* Register the policy of one non-owner principal kind in the caller's
|
|
40
|
+
* fiber; open snapshots are recomputed on registration and removal.
|
|
41
|
+
* @param kind - the principal kind the policy serves; never `owner`.
|
|
42
|
+
* @param policy - business-owned bindings and refusal reasons for principals of `kind`.
|
|
43
|
+
* @returns disposer removing this policy.
|
|
44
|
+
* @throws Error when `kind` already has a policy.
|
|
45
|
+
*/
|
|
46
|
+
registerPolicy<K extends Exclude<keyof PrincipalMap, 'owner'>>(kind: K, policy: RemoteAccessPolicy<PrincipalMap[K]>): () => void;
|
|
47
|
+
/**
|
|
48
|
+
* Report that the bindings or condition answers for `principal` may have
|
|
49
|
+
* changed. Business packages call this on every grant, revocation, mode
|
|
50
|
+
* change, and expiry; open snapshots re-push, and the next failure of each
|
|
51
|
+
* policy is logged again.
|
|
52
|
+
* @param principal - affected principal; omitted for every principal.
|
|
53
|
+
*/
|
|
54
|
+
invalidate(principal?: Principal): void;
|
|
55
|
+
/**
|
|
56
|
+
* Gateway-only: decide one request whose resources the Gateway resolved.
|
|
57
|
+
* The owner is allowed, and so is `remoteAccess/follow`: every Peer reads
|
|
58
|
+
* its own snapshot. Otherwise the action is allowed only when, on every
|
|
59
|
+
* listed resource, some binding of the principal's policy names that
|
|
60
|
+
* resource, has a role listing the action, and has no condition or a
|
|
61
|
+
* condition that allows. `bindings()` is called once per decision.
|
|
62
|
+
* @param request - the caller or recipient, the action, the resources it
|
|
63
|
+
* touches, and a call's arguments.
|
|
64
|
+
* @returns `allow`; or `deny` with `no-allow` when the principal kind has
|
|
65
|
+
* no policy, the first refusing condition's reason on the first resource
|
|
66
|
+
* no binding allows, else `explain()`'s reason, else `no-allow`;
|
|
67
|
+
* `policy-failed` when `bindings()`, a condition, or `explain()` throws,
|
|
68
|
+
* logged once per policy until the next `invalidate()`. The Gateway
|
|
69
|
+
* throws `remote-access/denied` for a denied call and drops a denied event.
|
|
70
|
+
*/
|
|
71
|
+
decide(request: RemoteAccessRequest<Principal>): RemoteAccessDecision;
|
|
72
|
+
/**
|
|
73
|
+
* Filter items inside a Remote method. The invocation is read from the
|
|
74
|
+
* Context that accessed this service, so call it as
|
|
75
|
+
* `this.ctx.remoteAccess.allows(resource)` from the Remote method. Each
|
|
76
|
+
* check reads the policy's bindings again, so a long-running stream sees
|
|
77
|
+
* binding changes from the next check on.
|
|
78
|
+
* @param resource - resource of one result item.
|
|
79
|
+
* @returns true for the owner and outside a Remote call; otherwise whether
|
|
80
|
+
* the invocation's endpoint action is allowed on `resource` without call
|
|
81
|
+
* arguments, false when the policy is absent or fails.
|
|
82
|
+
*/
|
|
83
|
+
allows(resource: RemoteAccessResource): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* The baseline a page opens with before its snapshot stream connects. A
|
|
86
|
+
* business package rendering a non-owner page assigns this value to
|
|
87
|
+
* {@link REMOTE_ACCESS_SNAPSHOT_KEY} after every index injection row,
|
|
88
|
+
* replacing the owner baseline the webServer index carries.
|
|
89
|
+
* @param principal - the page's caller identity.
|
|
90
|
+
* @returns the owner baseline for the owner; otherwise the principal's current grants.
|
|
91
|
+
*/
|
|
92
|
+
snapshotFor(principal: Principal): RemoteAccessBaseline;
|
|
93
|
+
/**
|
|
94
|
+
* Stream the calling Peer's access snapshot: one baseline, then `upsert`
|
|
95
|
+
* and `remove` frames after each `invalidate()` and each policy
|
|
96
|
+
* registration or removal that changes a grant. The owner receives only
|
|
97
|
+
* `{ owner: true }`. The generation ends when `signal` aborts or this
|
|
98
|
+
* service unloads, so a reloaded service starts every reader from a fresh
|
|
99
|
+
* baseline.
|
|
100
|
+
* @param signal - generation cancellation.
|
|
101
|
+
* @returns the access frames of one generation.
|
|
102
|
+
*/
|
|
103
|
+
follow(signal: AbortSignal): AsyncIterable<RemoteAccessFrame>;
|
|
104
|
+
/** One `bindings()` call; `undefined` after a logged failure. */
|
|
105
|
+
private bindingsOf;
|
|
106
|
+
/** Find a binding on `resource` whose role lists `action` and whose condition, if any, allows. */
|
|
107
|
+
private evaluate;
|
|
108
|
+
/** The refusal reason `explain()` names, else `no-allow`; `policy-failed` when it throws. */
|
|
109
|
+
private explain;
|
|
110
|
+
/**
|
|
111
|
+
* One grant per bound resource, in first-binding order: the endpoint
|
|
112
|
+
* actions of the resource's bindings that `decide()` would allow there
|
|
113
|
+
* without call arguments. A resource with no such action gets no grant;
|
|
114
|
+
* event actions stay out of the snapshot.
|
|
115
|
+
*/
|
|
116
|
+
private grantsFor;
|
|
117
|
+
/** Log a policy failure once per policy until the next `invalidate()`; snapshots and lists ask many times per change. */
|
|
118
|
+
private report;
|
|
119
|
+
}
|
|
120
|
+
export default RemoteAccessService;
|
|
121
|
+
//# sourceMappingURL=index.d.ts.map
|