@uuaid/agent-commons 0.0.0-stage → 0.2.0-alpha.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/CONFORMANCE.md +26 -0
- package/LICENSE +201 -0
- package/NOTICE +25 -0
- package/PROVENANCE.json +29 -0
- package/README.md +316 -2
- package/bin/agent-commons.mjs +71 -0
- package/examples/Dockerfile +12 -0
- package/examples/agent.mjs +17 -0
- package/examples/global-policy.json +9 -0
- package/examples/run-cohort.mjs +68 -0
- package/package.json +31 -4
- package/profiles/agentnet.chat.json +11 -0
- package/profiles/local.json +11 -0
- package/profiles/zilligon.com.json +11 -0
- package/profiles/zilligons.com.json +11 -0
- package/src/carrier.mjs +79 -0
- package/src/channels.d.mts +75 -0
- package/src/channels.mjs +158 -0
- package/src/cohort.mjs +61 -0
- package/src/collaboration.mjs +875 -0
- package/src/config.mjs +39 -0
- package/src/continuity.d.mts +24 -0
- package/src/continuity.mjs +594 -0
- package/src/governance-scope.d.mts +46 -0
- package/src/governance-scope.mjs +139 -0
- package/src/governance.mjs +353 -0
- package/src/index.d.ts +109 -0
- package/src/index.mjs +15 -0
- package/src/loop.mjs +71 -0
- package/src/offline.d.mts +7 -0
- package/src/offline.mjs +29 -0
- package/src/pillar.mjs +59 -0
- package/src/profiles.d.mts +12 -0
- package/src/profiles.mjs +91 -0
- package/src/runtime.mjs +346 -0
- package/src/store.mjs +73 -0
- package/src/transport.d.mts +76 -0
- package/src/transport.mjs +267 -0
- package/src/trust.mjs +65 -0
- package/src/vendor/pillar/blob/blob.mjs +510 -0
- package/src/vendor/pillar/crypto/e2e.mjs +97 -0
- package/src/vendor/pillar/identity/jcs.mjs +35 -0
- package/src/vendor/pillar/identity/keychain.mjs +192 -0
- package/src/vendor/pillar/identity/tier.mjs +130 -0
- package/src/vendor/pillar/net/carrier-client.mjs +197 -0
- package/src/vendor/pillar/net/delegation.mjs +209 -0
- package/src/vendor/pillar/net/envelope.mjs +258 -0
- package/src/vendor/pillar/net/refusals.mjs +127 -0
- package/src/vendor/pillar/net/seed-tier.mjs +185 -0
- package/test/channels.test.mjs +268 -0
- package/test/cohort.test.mjs +36 -0
- package/test/collaboration.test.mjs +459 -0
- package/test/continuity.test.mjs +229 -0
- package/test/custody.negative.test.mjs +165 -0
- package/test/derivation-parity.test.mjs +19 -0
- package/test/fixtures/derivation-vectors.json +26 -0
- package/test/fixtures/ed25519-small-order.vectors.json +15 -0
- package/test/governance-expected-red.expected-red.mjs +241 -0
- package/test/governance-scope.test.mjs +142 -0
- package/test/governance-t1-t10.test.mjs +477 -0
- package/test/governance.test.mjs +514 -0
- package/test/hold-admission.test.mjs +1185 -0
- package/test/keychain-strict-ed25519.test.mjs +240 -0
- package/test/offline.test.mjs +152 -0
- package/test/runtime-channels.test.mjs +1140 -0
- package/test/runtime.test.mjs +201 -0
- package/test/security-runtime.test.mjs +104 -0
- package/test/strict-ed25519-consumers.test.mjs +234 -0
- package/test/transport.test.mjs +353 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { CarrierClient, envelopeSha } from "./pillar.mjs";
|
|
3
|
+
import { isOffline, isLoopbackUrl } from "./offline.mjs";
|
|
4
|
+
|
|
5
|
+
let anonymousInstanceCounter = 0;
|
|
6
|
+
const activeLoopbackInstances = new Set();
|
|
7
|
+
|
|
8
|
+
function refusedCarrier(urls, env) {
|
|
9
|
+
if (!isOffline(env)) return null;
|
|
10
|
+
for (const url of urls ?? []) {
|
|
11
|
+
if (!isLoopbackUrl(url)) return url;
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function offlineError(url) {
|
|
17
|
+
const error = new Error(`offline: refused outbound carrier call to ${url}`);
|
|
18
|
+
error.code = "offline";
|
|
19
|
+
error.carrier = url;
|
|
20
|
+
return error;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* In-memory loopback transport for local unit testing and development.
|
|
25
|
+
* Never performs network operations; assigns stable instance-qualified source IDs.
|
|
26
|
+
* Enforces per-source FIFO execution for live subscriptions.
|
|
27
|
+
*/
|
|
28
|
+
export class MemoryLoopbackTransport {
|
|
29
|
+
#inboxes = new Map();
|
|
30
|
+
#subscribers = new Map();
|
|
31
|
+
#seq = 0;
|
|
32
|
+
#closed = false;
|
|
33
|
+
|
|
34
|
+
constructor(options = {}) {
|
|
35
|
+
if (options.instanceId !== undefined) {
|
|
36
|
+
if (typeof options.instanceId !== "string" || options.instanceId.trim() === "") {
|
|
37
|
+
throw new TypeError("instanceId must be a non-empty string when specified");
|
|
38
|
+
}
|
|
39
|
+
if (activeLoopbackInstances.has(options.instanceId)) {
|
|
40
|
+
const err = new Error(`Duplicate MemoryLoopbackTransport instance qualifier: ${options.instanceId}`);
|
|
41
|
+
err.code = "DUPLICATE_TRANSPORT_INSTANCE";
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
this.instanceId = options.instanceId;
|
|
45
|
+
} else {
|
|
46
|
+
anonymousInstanceCounter += 1;
|
|
47
|
+
this.instanceId = `auto-${anonymousInstanceCounter}-${randomUUID().slice(0, 8)}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
activeLoopbackInstances.add(this.instanceId);
|
|
51
|
+
this.sourceId = `memory://loopback/${this.instanceId}`;
|
|
52
|
+
|
|
53
|
+
this.capabilities = Object.freeze({
|
|
54
|
+
name: "memory",
|
|
55
|
+
canPush: true,
|
|
56
|
+
canPoll: true,
|
|
57
|
+
supportsStreaming: true,
|
|
58
|
+
maxMessageBytes: 64000,
|
|
59
|
+
loopbackSafe: true,
|
|
60
|
+
...(options.capabilities ?? {})
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
this.#inboxes.set(this.sourceId, []);
|
|
64
|
+
this.#subscribers.set(this.sourceId, new Set());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
sources() {
|
|
68
|
+
return [this.sourceId];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async deliver(envelope, options = {}) {
|
|
72
|
+
if (this.#closed) {
|
|
73
|
+
const err = new Error("Transport is closed");
|
|
74
|
+
err.code = "TRANSPORT_CLOSED";
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
if (!envelope || typeof envelope !== "object" || !envelope.id) {
|
|
78
|
+
const err = new Error("Invalid envelope: missing envelope id");
|
|
79
|
+
err.code = "INVALID_ENVELOPE";
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.#seq += 1;
|
|
84
|
+
const seq = this.#seq;
|
|
85
|
+
const sha = envelopeSha(envelope);
|
|
86
|
+
const item = { seq, envelope: structuredClone(envelope) };
|
|
87
|
+
|
|
88
|
+
const queue = this.#inboxes.get(this.sourceId);
|
|
89
|
+
queue.push(item);
|
|
90
|
+
|
|
91
|
+
const subs = this.#subscribers.get(this.sourceId);
|
|
92
|
+
if (subs && subs.size > 0) {
|
|
93
|
+
const event = { sourceId: this.sourceId, seq, envelope: structuredClone(envelope) };
|
|
94
|
+
for (const subRecord of subs) {
|
|
95
|
+
// Enqueue onto subscriber's promise tail to guarantee per-subscriber FIFO execution
|
|
96
|
+
subRecord.tail = subRecord.tail
|
|
97
|
+
.then(async () => {
|
|
98
|
+
await subRecord.callback(event);
|
|
99
|
+
})
|
|
100
|
+
.catch(() => {});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
accepted: true,
|
|
106
|
+
carrier: this.sourceId,
|
|
107
|
+
seq,
|
|
108
|
+
sha,
|
|
109
|
+
duplicate: false,
|
|
110
|
+
all: []
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async fetchInbox(sourceId, options = {}) {
|
|
115
|
+
if (this.#closed) {
|
|
116
|
+
const err = new Error("Transport is closed");
|
|
117
|
+
err.code = "TRANSPORT_CLOSED";
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
if (sourceId !== this.sourceId) {
|
|
121
|
+
const err = new Error(`Unknown source ID: ${sourceId}`);
|
|
122
|
+
err.code = "UNKNOWN_SOURCE_ID";
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const since = options.since ?? 0;
|
|
127
|
+
const queue = this.#inboxes.get(this.sourceId) ?? [];
|
|
128
|
+
const matching = queue.filter(item => item.seq > since);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
sourceId,
|
|
132
|
+
envelopes: matching.map(item => ({
|
|
133
|
+
sourceId,
|
|
134
|
+
seq: item.seq,
|
|
135
|
+
envelope: structuredClone(item.envelope)
|
|
136
|
+
})),
|
|
137
|
+
now: Date.now()
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
subscribe(sourceId, onEnvelope) {
|
|
142
|
+
if (this.#closed) {
|
|
143
|
+
const err = new Error("Transport is closed");
|
|
144
|
+
err.code = "TRANSPORT_CLOSED";
|
|
145
|
+
throw err;
|
|
146
|
+
}
|
|
147
|
+
if (sourceId !== this.sourceId) {
|
|
148
|
+
const err = new Error(`Unknown source ID: ${sourceId}`);
|
|
149
|
+
err.code = "UNKNOWN_SOURCE_ID";
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
if (typeof onEnvelope !== "function") {
|
|
153
|
+
throw new TypeError("onEnvelope callback must be a function");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const subRecord = {
|
|
157
|
+
callback: onEnvelope,
|
|
158
|
+
tail: Promise.resolve()
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const subs = this.#subscribers.get(this.sourceId);
|
|
162
|
+
subs.add(subRecord);
|
|
163
|
+
|
|
164
|
+
return () => {
|
|
165
|
+
subs.delete(subRecord);
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async close() {
|
|
170
|
+
this.#closed = true;
|
|
171
|
+
activeLoopbackInstances.delete(this.instanceId);
|
|
172
|
+
for (const subs of this.#subscribers.values()) {
|
|
173
|
+
subs.clear();
|
|
174
|
+
}
|
|
175
|
+
this.#subscribers.clear();
|
|
176
|
+
for (const queue of this.#inboxes.values()) {
|
|
177
|
+
queue.length = 0;
|
|
178
|
+
}
|
|
179
|
+
this.#inboxes.clear();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Adapter wrapping the package-owned guarded CarrierClient to conform to MessageTransport.
|
|
185
|
+
* Enforces offline refusal before ANY delivery or fetch, including for injected clients.
|
|
186
|
+
* Normalizes delivery receipts to include accepted: true and annotates inbox items with sourceId.
|
|
187
|
+
*/
|
|
188
|
+
export class PillarCarrierTransport {
|
|
189
|
+
constructor(options = {}) {
|
|
190
|
+
this._offlineEnv = options.env ?? options.client?._offlineEnv ?? process.env;
|
|
191
|
+
this.client = options.client ?? new CarrierClient({
|
|
192
|
+
keychain: options.keychain,
|
|
193
|
+
carriers: options.carriers,
|
|
194
|
+
env: this._offlineEnv
|
|
195
|
+
});
|
|
196
|
+
this.capabilities = Object.freeze({
|
|
197
|
+
name: "pillar",
|
|
198
|
+
canPush: false,
|
|
199
|
+
canPoll: true,
|
|
200
|
+
supportsStreaming: false,
|
|
201
|
+
maxMessageBytes: 512000,
|
|
202
|
+
loopbackSafe: false,
|
|
203
|
+
...(options.capabilities ?? {})
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
sources() {
|
|
208
|
+
return this.client.carriers ?? [];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async deliver(envelope, options = {}) {
|
|
212
|
+
// Enforce offline refusal before invoking any injected or vendored client
|
|
213
|
+
if (isOffline(this._offlineEnv)) {
|
|
214
|
+
const declared = this.sources();
|
|
215
|
+
if (declared.length === 0) {
|
|
216
|
+
throw offlineError("unspecified");
|
|
217
|
+
}
|
|
218
|
+
const blocked = refusedCarrier(declared, this._offlineEnv)
|
|
219
|
+
?? (envelope?.carrier ? refusedCarrier([envelope.carrier], this._offlineEnv) : null);
|
|
220
|
+
if (blocked) throw offlineError(blocked);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const res = await this.client.deliver(envelope, options);
|
|
224
|
+
const targetSources = this.sources().length ? this.sources() : (envelope?.carrier ? [envelope.carrier] : []);
|
|
225
|
+
return {
|
|
226
|
+
accepted: true,
|
|
227
|
+
carrier: res.carrier ?? targetSources[0] ?? "unknown",
|
|
228
|
+
seq: res.seq,
|
|
229
|
+
sha: res.sha ?? envelopeSha(envelope),
|
|
230
|
+
duplicate: res.duplicate ?? false,
|
|
231
|
+
all: res.all ?? []
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async fetchInbox(sourceId, options = {}) {
|
|
236
|
+
// Enforce offline refusal before invoking any injected or vendored client
|
|
237
|
+
if (isOffline(this._offlineEnv)) {
|
|
238
|
+
if (!sourceId) {
|
|
239
|
+
throw offlineError("unspecified");
|
|
240
|
+
}
|
|
241
|
+
const blocked = refusedCarrier([sourceId], this._offlineEnv);
|
|
242
|
+
if (blocked) throw offlineError(blocked);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const knownSources = this.sources();
|
|
246
|
+
if (knownSources.length > 0 && !knownSources.includes(sourceId)) {
|
|
247
|
+
const err = new Error(`Unexpected source ID: ${sourceId}`);
|
|
248
|
+
err.code = "UNEXPECTED_SOURCE_ID";
|
|
249
|
+
throw err;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const res = await this.client.fetchInbox(sourceId, options);
|
|
253
|
+
return {
|
|
254
|
+
sourceId,
|
|
255
|
+
envelopes: (res.envelopes ?? []).map(item => ({
|
|
256
|
+
sourceId,
|
|
257
|
+
seq: item.seq,
|
|
258
|
+
envelope: item.envelope
|
|
259
|
+
})),
|
|
260
|
+
now: res.now ?? Date.now()
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async close() {
|
|
265
|
+
// CarrierClient is stateless HTTP; no persistent resource to tear down.
|
|
266
|
+
}
|
|
267
|
+
}
|
package/src/trust.mjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { UuaidClient } from "@uuaid/sdk";
|
|
2
|
+
import { Keychain } from "./pillar.mjs";
|
|
3
|
+
import { isLoopbackUrl, isOffline } from "./offline.mjs";
|
|
4
|
+
|
|
5
|
+
export const REQUIRED_STANDARDS=["IAASO-1001","IAASO-2001","IAASO-3101","IAASO-3301"];
|
|
6
|
+
export class TrustError extends Error { constructor(code,message,{transient=false}={}){super(message);this.code=code;this.transient=transient} }
|
|
7
|
+
// Network/5xx/429/timeout failures of the trust backend are transient: callers must retry, never treat them as a verdict on the sender.
|
|
8
|
+
export function isTransientTrustFailure(error){if(error?.transient===true)return true;const status=error?.status??error?.statusCode;return error!=null&&!(error instanceof TrustError)&&(!Number.isInteger(status)||status>=500||status===408||status===429)}
|
|
9
|
+
async function backend(fn,code){try{return await fn()}catch(error){if(error instanceof TrustError)throw error;if(isTransientTrustFailure(error))throw new TrustError(code,`Trust backend unavailable: ${String(error?.message??error).slice(0,120)}`,{transient:true});throw error}}
|
|
10
|
+
export class RegistryTrust {
|
|
11
|
+
constructor({policy={},client,fetchImpl=fetch,env=process.env}={}) {
|
|
12
|
+
if(policy.mode!==undefined&&!["local","global"].includes(policy.mode))throw new TrustError("INVALID_POLICY_MODE","Policy mode must be explicitly local or global");
|
|
13
|
+
this.policy=policy;this.policy.mode??="local";this.env=env;
|
|
14
|
+
// One gate. policy.mode "local" skips only registry verification in authorize() (trust.mjs:44); policy.agents, key binding, and capability checks run in both modes; standards() still fetched (measured 2026-10-04).
|
|
15
|
+
this.fetch=async(url,init)=>{
|
|
16
|
+
if(isOffline(this.env,this.policy)&&!isLoopbackUrl(String(url)))throw new TrustError("OFFLINE","Offline mode refused an outbound fetch; no request was sent");
|
|
17
|
+
return fetchImpl(url,init);
|
|
18
|
+
};
|
|
19
|
+
this.client=client??new UuaidClient({baseUrl:policy.registryUrl??"https://api.uuaid.org",timeoutMs:10000,fetchImpl:this.fetch});
|
|
20
|
+
}
|
|
21
|
+
async standards(){
|
|
22
|
+
const pins=this.policy.standardPins??{};
|
|
23
|
+
if(REQUIRED_STANDARDS.some(code=>!/^[0-9a-f]{64}$/.test(pins[code]??"")))throw new TrustError("STANDARDS_UNPINNED","All required IAASO standard digests must be pinned before global admission");
|
|
24
|
+
const url=this.policy.authorityUrl??"https://authority.iaaso.org";
|
|
25
|
+
if(!url.startsWith("https://"))throw new TrustError("INSECURE_AUTHORITY","Global authority reads require HTTPS");
|
|
26
|
+
const response=await backend(()=>this.fetch(`${url.replace(/\/$/,"")}/v1/standards`,{signal:AbortSignal.timeout(10000)}),"AUTHORITY_UNAVAILABLE");
|
|
27
|
+
if(!response.ok)throw new TrustError("AUTHORITY_UNAVAILABLE","IAASO register could not be read",{transient:response.status>=500||response.status===429||response.status===408});
|
|
28
|
+
const list=(await backend(()=>response.json(),"AUTHORITY_UNAVAILABLE")).standards;
|
|
29
|
+
if(!Array.isArray(list))throw new TrustError("REGISTER_SHAPE","Invalid IAASO register response");
|
|
30
|
+
for(const code of REQUIRED_STANDARDS){
|
|
31
|
+
const record=list.find(r=>r.code===code);
|
|
32
|
+
if(record?.stage!=="published"||record.content_hash!==pins[code])throw new TrustError("STANDARD_PIN_MISMATCH",`${code} is not published at the configured digest`);
|
|
33
|
+
}
|
|
34
|
+
return list;
|
|
35
|
+
}
|
|
36
|
+
async authorize(uuaid,publicKey,capability="commons:message"){
|
|
37
|
+
if(!/^uuaid:[a-z0-9-]+:agent:[0-9a-f-]{36}$/.test(uuaid)||!/^[0-9a-f]{64}$/.test(publicKey))throw new TrustError("AGENT_ONLY","A cryptographically bound agent UUAID is required");
|
|
38
|
+
if(Keychain.localIdFromKey(Buffer.from(publicKey,"hex"))!==uuaid.split(":")[3])throw new TrustError("KEY_BINDING","Agent UUAID is not owned by the presented key");
|
|
39
|
+
if(this.policy.blocked?.includes(uuaid))throw new TrustError("SUBJECT_BLOCKED","Subject is blocked");
|
|
40
|
+
const binding=this.policy.agents?.[uuaid];
|
|
41
|
+
// One key must be one voice: reject any policy that admits the same key under several UUAID spellings.
|
|
42
|
+
if(binding&&Object.values(this.policy.agents).filter(a=>a?.publicKey===publicKey).length!==1)throw new TrustError("DUPLICATE_KEY","One public key is admitted under more than one UUAID; Sybil-ambiguous policy is denied");
|
|
43
|
+
if(!binding||binding.publicKey!==publicKey||binding.kind!=="agent"||!binding.capabilities?.includes(capability))throw new TrustError("NOT_ADMITTED","Subject/key/capability has not been admitted by this deployment");
|
|
44
|
+
if(this.policy.mode!=="global")return {tier:"local-policy-pinned",uuaid,capability,certified:false};
|
|
45
|
+
if(!(this.policy.registryUrl??"https://api.uuaid.org").startsWith("https://"))throw new TrustError("INSECURE_REGISTRY","Global registry verification requires HTTPS");
|
|
46
|
+
await this.standards();
|
|
47
|
+
const resolution=await backend(()=>this.client.resolve(uuaid),"REGISTRY_UNAVAILABLE");
|
|
48
|
+
const agent=resolution?.agent??resolution?.subject??resolution;
|
|
49
|
+
if(agent?.uuaid!==uuaid||agent.status!=="active")throw new TrustError("SUBJECT_INACTIVE","Registry subject is not explicitly active");
|
|
50
|
+
if(!binding.credentialId)throw new TrustError("CREDENTIAL_REQUIRED","Global agent admission requires a pinned credential");
|
|
51
|
+
const verdict=await backend(()=>this.client.verify(binding.credentialId),"REGISTRY_UNAVAILABLE");
|
|
52
|
+
if(verdict.credential_id!==binding.credentialId||verdict.agent_uuaid!==uuaid||![verdict.valid,verdict.signatureValid,verdict.active,verdict.notExpired].every(v=>v===true))throw new TrustError("CREDENTIAL_INVALID","Credential signature, subject, active status, or expiry failed");
|
|
53
|
+
// Capabilities are operator policy grants. A valid credential alone does not
|
|
54
|
+
// imply an IAASO accreditation role or permission to ratify a standard.
|
|
55
|
+
return {tier:"registry-credential-and-policy",uuaid,capability,credentialId:binding.credentialId,certified:false};
|
|
56
|
+
}
|
|
57
|
+
async publishedProfilePin(code,profileHash){
|
|
58
|
+
await this.standards();
|
|
59
|
+
const response=await backend(()=>this.fetch(`${(this.policy.authorityUrl??"https://authority.iaaso.org").replace(/\/$/,"")}/v1/standards`,{signal:AbortSignal.timeout(10000)}),"AUTHORITY_UNAVAILABLE");
|
|
60
|
+
if(!response.ok)throw new TrustError("AUTHORITY_UNAVAILABLE","Cannot inspect global profile authority",{transient:response.status>=500||response.status===429||response.status===408});
|
|
61
|
+
const record=(await backend(()=>response.json(),"AUTHORITY_UNAVAILABLE")).standards?.find(s=>s.code===code);
|
|
62
|
+
if(record?.stage!=="published"||record.content_hash!==profileHash)throw new TrustError("GLOBAL_NOT_RATIFIED","The exact global profile document is not published at this IAASO pin");
|
|
63
|
+
return record;
|
|
64
|
+
}
|
|
65
|
+
}
|