@consiliency/agent-board-schema 1.3.0 → 1.4.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/bin/agent-board-schema.js +0 -0
- package/fixtures/already-applied.json +104 -0
- package/migrations/20260717000100_agent_board_board_role.sql +180 -0
- package/migrations/20260717000200_agent_board_thread_acl.sql +149 -0
- package/migrations/20260717000300_agent_board_agent_lineage.sql +54 -0
- package/migrations/20260717000400_agent_board_delegation_policies.sql +175 -0
- package/migrations/20260718000100_agent_board_send_authz_config.sql +172 -0
- package/migrations/20260718000200_agent_board_send_role_gate.sql +804 -0
- package/migrations/20260718000300_agent_board_fleet_policy_default_deny.sql +192 -0
- package/migrations/20260718000400_agent_board_grant_ceiling_wiring.sql +58 -0
- package/migrations/20260718000500_agent_board_fleet_agent_session_class.sql +24 -0
- package/migrations/20260718000600_agent_board_mint_fleet_agent_session.sql +172 -0
- package/migrations/20260718000700_agent_board_nonfleet_session_reaper.sql +35 -0
- package/migrations/20260718000800_agent_board_regrant_scoped_send.sql +24 -0
- package/migrations/20260718000900_agent_board_agents_status_revoked.sql +24 -0
- package/migrations/20260718001000_agent_board_delegation_enforcement.sql +296 -0
- package/migrations/20260718001100_agent_board_revoke_agent_subtree.sql +238 -0
- package/migrations/20260718001200_agent_board_resigning_refresh.sql +129 -0
- package/migrations/20260718001300_agent_board_identity_providers.sql +70 -0
- package/migrations/20260718001400_agent_board_enrollment_federation_audit.sql +50 -0
- package/migrations/20260718001500_agent_board_enroll_federated_identity.sql +220 -0
- package/migrations/20260719000100_agent_board_signing_challenge_pending_cap.sql +124 -0
- package/migrations/20260719000200_agent_board_signing_challenge_reaper.sql +34 -0
- package/migrations/20260719000300_agent_board_enroll_rate_cap.sql +72 -0
- package/migrations/20260719001000_agent_board_identity_providers_portal_contract.sql +38 -0
- package/migrations/20260719001100_agent_board_enroll_federated_identity_pop.sql +267 -0
- package/migrations/20260719001200_agent_board_enroll_federated_identity_raw_fingerprint.sql +296 -0
- package/migrations/20260719001300_agent_board_enroll_rate_events_reaper_rls.sql +62 -0
- package/package.json +9 -9
- package/schema-manifest.json +238 -4
- package/src/tooling.test.mjs +42 -1
- package/tests/agent_board_agent_lineage.test.sql +73 -0
- package/tests/agent_board_agents_status_revoked.test.sql +60 -0
- package/tests/agent_board_auth_rpcs.test.sql +17 -16
- package/tests/agent_board_board_role.test.sql +129 -0
- package/tests/agent_board_bundle_endpoint_compat.test.sql +18 -6
- package/tests/agent_board_canary_signed_fleet_enforcement.test.sql +8 -8
- package/tests/agent_board_delegation_enforcement.test.sql +175 -0
- package/tests/agent_board_delegation_policies.test.sql +119 -0
- package/tests/agent_board_enroll_federated_identity.test.sql +217 -0
- package/tests/agent_board_enroll_pop_binding.test.sql +131 -0
- package/tests/agent_board_enroll_rate_cap.test.sql +53 -0
- package/tests/agent_board_enrollment_audit_content.test.sql +134 -0
- package/tests/agent_board_fleet_agent_session_class.test.sql +64 -0
- package/tests/agent_board_fleet_policy_default_deny.test.sql +149 -0
- package/tests/agent_board_fleet_registry.test.sql +2 -2
- package/tests/agent_board_identity_providers.test.sql +104 -0
- package/tests/agent_board_inbox_rpcs.test.sql +6 -6
- package/tests/agent_board_mint_fleet_agent_session.test.sql +290 -0
- package/tests/agent_board_nonfleet_session_reaper.test.sql +61 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +130 -0
- package/tests/agent_board_resigning_refresh.test.sql +73 -0
- package/tests/agent_board_revoke_agent_subtree.test.sql +188 -0
- package/tests/agent_board_schema_version.test.sql +2 -2
- package/tests/agent_board_schema_version_rpc.test.sql +1 -1
- package/tests/agent_board_scoped_send_regrant.test.sql +138 -0
- package/tests/agent_board_send_authz_config.test.sql +84 -0
- package/tests/agent_board_send_authz_matrix.test.sql +159 -0
- package/tests/agent_board_send_role_gate.test.sql +177 -0
- package/tests/agent_board_signing_challenge_pending_cap.test.sql +87 -0
- package/tests/agent_board_signing_challenge_reaper.test.sql +74 -0
- package/tests/agent_board_thread_acl.test.sql +129 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
-- MBPORTALGATE SL-2 (IF-0-MBPORTALGATE-4): rework enroll_federated_identity to the
|
|
2
|
+
-- real principal model — proof-of-possession at enrollment, one human -> N agents.
|
|
3
|
+
--
|
|
4
|
+
-- WHAT CHANGES vs the prior 8-arg RPC (20260718001500):
|
|
5
|
+
-- * The Portal credential is a VERIFIED access-token JWT; the /enroll EF ALSO
|
|
6
|
+
-- Ed25519-verifies a proof-of-possession signature over the token bytes and passes
|
|
7
|
+
-- p_pop_verified=true (mirrors record_signing_key_proof_result's verify/record
|
|
8
|
+
-- split). This RPC re-gates p_pop_verified and refuses false.
|
|
9
|
+
-- * The subject (sub) establishes a HUMAN OWNER auth.users row idempotently per
|
|
10
|
+
-- (issuer, sub), recorded as agents.owner_user_id. The AGENT is a SEPARATE agents
|
|
11
|
+
-- row identified by the enrolled Ed25519 key — NEVER keyed on sub. Enrolling the
|
|
12
|
+
-- same human (issuer, sub) with a second key creates a SECOND agent and does NOT
|
|
13
|
+
-- mutate the first. Each agent gets its OWN auth_user_id (session-usable via its
|
|
14
|
+
-- enrolled key, exactly like provision_agent), so revoking one agent revokes
|
|
15
|
+
-- neither the human nor a sibling.
|
|
16
|
+
-- * The enrolled public key lands in agent_signing_keys as status='active',
|
|
17
|
+
-- proof_state='proven' (satisfies agent_signing_keys_active_requires_proof). The
|
|
18
|
+
-- RPC RECOMPUTES the sha256:base64url fingerprint in-DB from p_agent_public_key and
|
|
19
|
+
-- ASSERTS it equals p_agent_public_key_fingerprint (defense-in-depth against a
|
|
20
|
+
-- direct service_role caller — the EF is the trusted computor, the RPC re-gates),
|
|
21
|
+
-- and dedups on the agent_signing_keys.fingerprint UNIQUE index (duplicate -> raise).
|
|
22
|
+
--
|
|
23
|
+
-- Returned auth_user_id = the AGENT's own board principal (session-usable), NOT the
|
|
24
|
+
-- human owner (the human is owner_user_id). This is the sanctioned impl choice
|
|
25
|
+
-- IF-0-MBPORTALGATE-4 defers to pgTAP, pinned by the N-agents + revoke-isolation cases.
|
|
26
|
+
--
|
|
27
|
+
-- Fingerprint derivation is byte-identical to the delegation-enforcement child-key
|
|
28
|
+
-- fingerprint (20260718001000): sha256 over the UTF-8 bytes of the base64url public-key
|
|
29
|
+
-- STRING, base64url-encoded (unpadded), 'sha256:'-prefixed — so the EF's WebCrypto
|
|
30
|
+
-- computation and this DB computation round-trip exactly.
|
|
31
|
+
--
|
|
32
|
+
-- service_role-only. Additive forward-only migration; writes no schema_version (stays 8).
|
|
33
|
+
-- The prior 8-arg function is DROPped (return type widens 4->5 cols, so create-or-replace
|
|
34
|
+
-- cannot be used) and replaced. No migration/EF/test FILE is deleted.
|
|
35
|
+
|
|
36
|
+
drop function if exists agent_board.enroll_federated_identity(
|
|
37
|
+
text, text, text, jsonb, text, text, text[], text[]);
|
|
38
|
+
|
|
39
|
+
create or replace function agent_board.enroll_federated_identity(
|
|
40
|
+
p_issuer text,
|
|
41
|
+
p_audience text,
|
|
42
|
+
p_subject text,
|
|
43
|
+
p_human_identity_claim jsonb,
|
|
44
|
+
p_slug text,
|
|
45
|
+
p_agent_public_key text,
|
|
46
|
+
p_agent_public_key_format text,
|
|
47
|
+
p_agent_public_key_fingerprint text,
|
|
48
|
+
p_pop_verified boolean,
|
|
49
|
+
p_actor_sub text default null,
|
|
50
|
+
p_agent_kind text default 'agent',
|
|
51
|
+
p_requested_scopes text[] default '{}',
|
|
52
|
+
p_requested_board_scopes text[] default '{}'
|
|
53
|
+
)
|
|
54
|
+
returns table(
|
|
55
|
+
agent_id uuid,
|
|
56
|
+
auth_user_id uuid,
|
|
57
|
+
provider_id text,
|
|
58
|
+
signing_key_id uuid,
|
|
59
|
+
granted_scopes text[]
|
|
60
|
+
)
|
|
61
|
+
language plpgsql
|
|
62
|
+
security definer
|
|
63
|
+
set search_path = agent_board, public
|
|
64
|
+
as $$
|
|
65
|
+
declare
|
|
66
|
+
v_slug text;
|
|
67
|
+
v_kind text;
|
|
68
|
+
v_provider agent_board.identity_providers%rowtype;
|
|
69
|
+
v_owner_user_id uuid;
|
|
70
|
+
v_auth_user_id uuid;
|
|
71
|
+
v_agent_handle text;
|
|
72
|
+
v_agent_id uuid;
|
|
73
|
+
v_signing_key_id uuid;
|
|
74
|
+
v_fingerprint text;
|
|
75
|
+
v_key_hex text;
|
|
76
|
+
v_requested_scopes text[] := coalesce(p_requested_scopes, '{}');
|
|
77
|
+
v_requested_board_scopes text[] := coalesce(p_requested_board_scopes, '{}');
|
|
78
|
+
begin
|
|
79
|
+
-- service_role-only (defense in depth on top of the execute-grant revocation).
|
|
80
|
+
if not agent_board._is_service_role() then
|
|
81
|
+
raise exception 'service_role required for enroll_federated_identity' using errcode = '42501';
|
|
82
|
+
end if;
|
|
83
|
+
|
|
84
|
+
-- A verified subject + human identity claim are always present from the EF.
|
|
85
|
+
if p_subject is null then
|
|
86
|
+
raise exception 'verified subject is required' using errcode = '22023';
|
|
87
|
+
end if;
|
|
88
|
+
if p_human_identity_claim is null then
|
|
89
|
+
raise exception 'human identity claim is required' using errcode = '22023';
|
|
90
|
+
end if;
|
|
91
|
+
|
|
92
|
+
-- (0) PoP gate — the EF Ed25519-verified the proof-of-possession over the token
|
|
93
|
+
-- bytes and passes true. Refuse false BEFORE any write (no agent/key/audit row).
|
|
94
|
+
if p_pop_verified is not true then
|
|
95
|
+
raise exception 'proof-of-possession was not verified; enrollment refused'
|
|
96
|
+
using errcode = '42501';
|
|
97
|
+
end if;
|
|
98
|
+
|
|
99
|
+
-- (1) Public-key shape gate. The enroll contract is raw Ed25519 (32 bytes -> 43
|
|
100
|
+
-- base64url chars). Reject the all-zero small-order placeholder.
|
|
101
|
+
if p_agent_public_key_format is distinct from 'raw_ed25519_base64url' then
|
|
102
|
+
raise exception 'agent public key format must be raw_ed25519_base64url' using errcode = '22023';
|
|
103
|
+
end if;
|
|
104
|
+
if p_agent_public_key is null or p_agent_public_key !~ '^[A-Za-z0-9_-]{43}$' then
|
|
105
|
+
raise exception 'malformed agent public key: expected a 43-char base64url raw Ed25519 key'
|
|
106
|
+
using errcode = '22023';
|
|
107
|
+
end if;
|
|
108
|
+
if p_agent_public_key = repeat('A', 43) then
|
|
109
|
+
raise exception 'agent public key is the all-zero small-order placeholder and is rejected'
|
|
110
|
+
using errcode = '22023';
|
|
111
|
+
end if;
|
|
112
|
+
|
|
113
|
+
-- (2) Recompute-and-assert the fingerprint in-DB (byte-identical to the EF/WebCrypto
|
|
114
|
+
-- and to the delegation-enforcement derivation). A tampered fingerprint -> raise.
|
|
115
|
+
v_fingerprint := 'sha256:' || rtrim(
|
|
116
|
+
translate(encode(extensions.digest(convert_to(p_agent_public_key, 'UTF8'), 'sha256'), 'base64'), '+/', '-_'),
|
|
117
|
+
'=');
|
|
118
|
+
if p_agent_public_key_fingerprint is distinct from v_fingerprint then
|
|
119
|
+
raise exception 'agent public key fingerprint mismatch (recompute != supplied)'
|
|
120
|
+
using errcode = '22023';
|
|
121
|
+
end if;
|
|
122
|
+
|
|
123
|
+
-- (3) act.sub is audit-only, but when present it must be a real value.
|
|
124
|
+
if p_actor_sub is not null and length(trim(p_actor_sub)) = 0 then
|
|
125
|
+
raise exception 'actor sub, when present, must be non-empty' using errcode = '22023';
|
|
126
|
+
end if;
|
|
127
|
+
|
|
128
|
+
v_slug := agent_board._normalize_provision_slug(p_slug);
|
|
129
|
+
v_kind := lower(coalesce(trim(p_agent_kind), ''));
|
|
130
|
+
|
|
131
|
+
-- (4) allowlist lookup by (issuer, audience); must exist + supported + policy-approved.
|
|
132
|
+
select *
|
|
133
|
+
into v_provider
|
|
134
|
+
from agent_board.identity_providers
|
|
135
|
+
where issuer = p_issuer
|
|
136
|
+
and audience = p_audience
|
|
137
|
+
limit 1;
|
|
138
|
+
if v_provider.id is null then
|
|
139
|
+
raise exception 'issuer/audience not in the identity-provider allowlist' using errcode = '42501';
|
|
140
|
+
end if;
|
|
141
|
+
if not v_provider.supported then
|
|
142
|
+
raise exception 'identity provider is not supported' using errcode = '42501';
|
|
143
|
+
end if;
|
|
144
|
+
if not v_provider.policy_approved then
|
|
145
|
+
raise exception 'identity provider is not policy-approved' using errcode = '42501';
|
|
146
|
+
end if;
|
|
147
|
+
|
|
148
|
+
-- (5) scope-ceiling + kind gate. 'system' is never federatable.
|
|
149
|
+
if v_kind = 'system' then
|
|
150
|
+
raise exception 'the system kind cannot be federated' using errcode = '42501';
|
|
151
|
+
end if;
|
|
152
|
+
if v_kind not in ('human', 'agent') then
|
|
153
|
+
raise exception 'agent kind must be human or agent' using errcode = '22023';
|
|
154
|
+
end if;
|
|
155
|
+
if array_length(v_provider.allowed_agent_kinds, 1) is not null
|
|
156
|
+
and not (v_kind = any (v_provider.allowed_agent_kinds)) then
|
|
157
|
+
raise exception 'agent kind % is outside the provider ceiling', v_kind using errcode = '42501';
|
|
158
|
+
end if;
|
|
159
|
+
if not (v_requested_scopes <@ v_provider.allowed_scopes) then
|
|
160
|
+
raise exception 'requested scopes exceed the provider allowed_scopes ceiling' using errcode = '42501';
|
|
161
|
+
end if;
|
|
162
|
+
if not (v_requested_board_scopes <@ v_provider.allowed_board_scopes) then
|
|
163
|
+
raise exception 'requested board scopes exceed the provider allowed_board_scopes ceiling' using errcode = '42501';
|
|
164
|
+
end if;
|
|
165
|
+
|
|
166
|
+
-- (6) dedup on the enrolled key: a fingerprint already enrolled cannot be re-bound.
|
|
167
|
+
-- Pre-check for a clean error; the UNIQUE index is the hard guarantee.
|
|
168
|
+
if exists (select 1 from agent_board.agent_signing_keys k where k.fingerprint = v_fingerprint) then
|
|
169
|
+
raise exception 'this signing key is already enrolled (duplicate fingerprint)'
|
|
170
|
+
using errcode = '23505';
|
|
171
|
+
end if;
|
|
172
|
+
|
|
173
|
+
-- (7) HUMAN OWNER — established idempotently per (issuer, sub) so re-enrollment of the
|
|
174
|
+
-- same human converges to ONE owner auth.users row (recorded as owner_user_id).
|
|
175
|
+
v_owner_user_id := agent_board._ensure_agent_auth_user(
|
|
176
|
+
'fed-human-' || substr(encode(extensions.digest(convert_to(p_issuer || '|' || p_subject, 'UTF8'), 'sha256'), 'hex'), 1, 32));
|
|
177
|
+
|
|
178
|
+
-- (8) AGENT handle — unique per enrolled key. Use the slug; on collision (a human's
|
|
179
|
+
-- 2nd agent under the same slug, or an unrelated handle) disambiguate with a
|
|
180
|
+
-- deterministic per-key hex fragment so a distinct agent row is always created.
|
|
181
|
+
v_key_hex := encode(extensions.digest(convert_to(p_agent_public_key, 'UTF8'), 'sha256'), 'hex');
|
|
182
|
+
v_agent_handle := v_slug;
|
|
183
|
+
if exists (select 1 from agent_board.agents a where a.handle = v_agent_handle) then
|
|
184
|
+
v_agent_handle := v_slug || '-' || substr(v_key_hex, 1, 12);
|
|
185
|
+
end if;
|
|
186
|
+
|
|
187
|
+
-- (9) the agent's OWN board principal (session-usable via its enrolled key).
|
|
188
|
+
v_auth_user_id := agent_board._ensure_agent_auth_user(v_agent_handle);
|
|
189
|
+
|
|
190
|
+
-- (10) create the SEPARATE agent row (never keyed on sub; owner = the human).
|
|
191
|
+
insert into agent_board.agents(
|
|
192
|
+
agent_type,
|
|
193
|
+
kind,
|
|
194
|
+
handle,
|
|
195
|
+
display_name,
|
|
196
|
+
status,
|
|
197
|
+
auth_user_id,
|
|
198
|
+
owner_user_id,
|
|
199
|
+
metadata
|
|
200
|
+
)
|
|
201
|
+
values (
|
|
202
|
+
v_kind,
|
|
203
|
+
v_kind::agent_board.agent_kind,
|
|
204
|
+
v_agent_handle,
|
|
205
|
+
agent_board._default_provision_display_name(v_agent_handle),
|
|
206
|
+
'active',
|
|
207
|
+
v_auth_user_id,
|
|
208
|
+
v_owner_user_id,
|
|
209
|
+
jsonb_build_object(
|
|
210
|
+
'federation', jsonb_build_object(
|
|
211
|
+
'issuer', p_issuer,
|
|
212
|
+
'audience', p_audience,
|
|
213
|
+
'subject', p_subject,
|
|
214
|
+
'actorSub', p_actor_sub,
|
|
215
|
+
'providerId', v_provider.id,
|
|
216
|
+
'signingKeyFingerprint', v_fingerprint,
|
|
217
|
+
'requestedScopes', to_jsonb(v_requested_scopes),
|
|
218
|
+
'requestedBoardScopes', to_jsonb(v_requested_board_scopes),
|
|
219
|
+
'popVerified', true,
|
|
220
|
+
'enrolledAt', now()
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
returning id into v_agent_id;
|
|
225
|
+
|
|
226
|
+
-- (11) enroll the public key as active + proven (satisfies active_requires_proof).
|
|
227
|
+
insert into agent_board.agent_signing_keys(
|
|
228
|
+
id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint,
|
|
229
|
+
status, proof_state, metadata
|
|
230
|
+
)
|
|
231
|
+
values (
|
|
232
|
+
gen_random_uuid(), v_agent_id, 'portal-enroll', 1, p_agent_public_key,
|
|
233
|
+
p_agent_public_key_format, v_fingerprint,
|
|
234
|
+
'active', 'proven',
|
|
235
|
+
jsonb_build_object('enrolledVia', 'federated_identity', 'popVerified', true)
|
|
236
|
+
)
|
|
237
|
+
returning id into v_signing_key_id;
|
|
238
|
+
|
|
239
|
+
-- (12) one static claim-snapshot audit row (the verified claim contains act.sub).
|
|
240
|
+
insert into agent_board.enrollment_federation_audit(
|
|
241
|
+
issuer, audience, subject, human_identity_claim, provider_id,
|
|
242
|
+
agent_id, auth_user_id, requested_scopes, granted_scopes
|
|
243
|
+
)
|
|
244
|
+
values (
|
|
245
|
+
p_issuer, p_audience, p_subject, p_human_identity_claim, v_provider.id,
|
|
246
|
+
v_agent_id, v_auth_user_id, v_requested_scopes, v_requested_scopes
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
return query
|
|
250
|
+
select v_agent_id, v_auth_user_id, v_provider.id, v_signing_key_id, v_requested_scopes;
|
|
251
|
+
end;
|
|
252
|
+
$$;
|
|
253
|
+
|
|
254
|
+
comment on function agent_board.enroll_federated_identity(
|
|
255
|
+
text, text, text, jsonb, text, text, text, text, boolean, text, text, text[], text[]) is
|
|
256
|
+
'PoP-at-enrollment federated identity: re-gates p_pop_verified + allowlist/policy/scope, '
|
|
257
|
+
'recomputes-and-asserts the key fingerprint, establishes ONE human owner (owner_user_id) '
|
|
258
|
+
'idempotently per (issuer,sub), creates a SEPARATE key-identified agent (own auth_user_id) '
|
|
259
|
+
'per enrolled key, and enrolls the key active+proven. service_role-only; returns a '
|
|
260
|
+
'board-native agent + signing_key_id, never a Portal token.';
|
|
261
|
+
|
|
262
|
+
revoke all on function agent_board.enroll_federated_identity(
|
|
263
|
+
text, text, text, jsonb, text, text, text, text, boolean, text, text, text[], text[])
|
|
264
|
+
from public, anon, authenticated;
|
|
265
|
+
grant execute on function agent_board.enroll_federated_identity(
|
|
266
|
+
text, text, text, jsonb, text, text, text, text, boolean, text, text, text[], text[])
|
|
267
|
+
to service_role;
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
-- MBPORTAL PR #32 reconcile (B1 + B2): server-authoritative identity + canonical
|
|
2
|
+
-- raw-byte fingerprint for enroll_federated_identity.
|
|
3
|
+
--
|
|
4
|
+
-- WHY (two blocking-class defects the mock hid, both falsifying "proven end-to-end"):
|
|
5
|
+
--
|
|
6
|
+
-- B1 — the enrolled agent could never mint a /session. The RPC stores the enrolled
|
|
7
|
+
-- key as key_id='portal-enroll', key_version=1 and (on handle collision) rewrites the
|
|
8
|
+
-- agent handle, but the prior return table surfaced NEITHER, so the client kept its
|
|
9
|
+
-- locally-generated key_id + the raw input slug and /session (which resolves by the
|
|
10
|
+
-- exact (agents.handle, key_id, key_version)) found no row. FIX: the server is
|
|
11
|
+
-- authoritative for identity — the return table now ALSO yields key_id, key_version,
|
|
12
|
+
-- and the post-normalization/disambiguation agent_handle, and the client mints
|
|
13
|
+
-- /session with exactly those (keeping only its private key from the keystore).
|
|
14
|
+
--
|
|
15
|
+
-- B2 — the fingerprint (the sole uniqueness/dedup/revocation key) was computed over
|
|
16
|
+
-- the base64url STRING, not the decoded key bytes. A 32-byte Ed25519 key's 43-char
|
|
17
|
+
-- base64url encoding has 2 don't-care low bits, so up to 4 distinct strings decode to
|
|
18
|
+
-- the SAME key, all pass the regex, all verify one PoP signature, yet hash to 4
|
|
19
|
+
-- different fingerprints -> the UNIQUE index admits 4 rows: one keypair backing up to
|
|
20
|
+
-- 4 agents, and revoking one leaves the siblings alive. FIX: decode agent_public_key
|
|
21
|
+
-- to 32 raw bytes, REJECT if a canonical re-encode != the input string (kills the
|
|
22
|
+
-- 4-strings-one-key aliasing and the non-canonical all-zero-placeholder bypass), and
|
|
23
|
+
-- compute the fingerprint over the RAW 32 bytes. This matches the client child_keygen
|
|
24
|
+
-- fingerprint (already raw-byte) and is mirrored in the /enroll EF.
|
|
25
|
+
--
|
|
26
|
+
-- Additive, forward-only. The return table WIDENS 5 -> 8 columns, so create-or-replace
|
|
27
|
+
-- cannot be used: the released 13-arg function (20260719001100) is DROPped and recreated
|
|
28
|
+
-- here. The released 001100 bytes are NOT edited. Args are unchanged (still 13); the
|
|
29
|
+
-- grant/revoke signatures are identical. Writes no schema_version (stays 8).
|
|
30
|
+
|
|
31
|
+
drop function if exists agent_board.enroll_federated_identity(
|
|
32
|
+
text, text, text, jsonb, text, text, text, text, boolean, text, text, text[], text[]);
|
|
33
|
+
|
|
34
|
+
create or replace function agent_board.enroll_federated_identity(
|
|
35
|
+
p_issuer text,
|
|
36
|
+
p_audience text,
|
|
37
|
+
p_subject text,
|
|
38
|
+
p_human_identity_claim jsonb,
|
|
39
|
+
p_slug text,
|
|
40
|
+
p_agent_public_key text,
|
|
41
|
+
p_agent_public_key_format text,
|
|
42
|
+
p_agent_public_key_fingerprint text,
|
|
43
|
+
p_pop_verified boolean,
|
|
44
|
+
p_actor_sub text default null,
|
|
45
|
+
p_agent_kind text default 'agent',
|
|
46
|
+
p_requested_scopes text[] default '{}',
|
|
47
|
+
p_requested_board_scopes text[] default '{}'
|
|
48
|
+
)
|
|
49
|
+
returns table(
|
|
50
|
+
agent_id uuid,
|
|
51
|
+
auth_user_id uuid,
|
|
52
|
+
provider_id text,
|
|
53
|
+
signing_key_id uuid,
|
|
54
|
+
granted_scopes text[],
|
|
55
|
+
key_id text,
|
|
56
|
+
key_version int,
|
|
57
|
+
agent_handle text
|
|
58
|
+
)
|
|
59
|
+
language plpgsql
|
|
60
|
+
security definer
|
|
61
|
+
set search_path = agent_board, public
|
|
62
|
+
as $$
|
|
63
|
+
declare
|
|
64
|
+
v_slug text;
|
|
65
|
+
v_kind text;
|
|
66
|
+
v_provider agent_board.identity_providers%rowtype;
|
|
67
|
+
v_owner_user_id uuid;
|
|
68
|
+
v_auth_user_id uuid;
|
|
69
|
+
v_agent_handle text;
|
|
70
|
+
v_agent_id uuid;
|
|
71
|
+
v_signing_key_id uuid;
|
|
72
|
+
v_fingerprint text;
|
|
73
|
+
v_key_hex text;
|
|
74
|
+
v_raw_key bytea;
|
|
75
|
+
v_reencoded text;
|
|
76
|
+
v_key_id text := 'portal-enroll';
|
|
77
|
+
v_key_version int := 1;
|
|
78
|
+
v_requested_scopes text[] := coalesce(p_requested_scopes, '{}');
|
|
79
|
+
v_requested_board_scopes text[] := coalesce(p_requested_board_scopes, '{}');
|
|
80
|
+
begin
|
|
81
|
+
-- service_role-only (defense in depth on top of the execute-grant revocation).
|
|
82
|
+
if not agent_board._is_service_role() then
|
|
83
|
+
raise exception 'service_role required for enroll_federated_identity' using errcode = '42501';
|
|
84
|
+
end if;
|
|
85
|
+
|
|
86
|
+
-- A verified subject + human identity claim are always present from the EF.
|
|
87
|
+
if p_subject is null then
|
|
88
|
+
raise exception 'verified subject is required' using errcode = '22023';
|
|
89
|
+
end if;
|
|
90
|
+
if p_human_identity_claim is null then
|
|
91
|
+
raise exception 'human identity claim is required' using errcode = '22023';
|
|
92
|
+
end if;
|
|
93
|
+
|
|
94
|
+
-- (0) PoP gate — the EF Ed25519-verified the proof-of-possession over the token
|
|
95
|
+
-- bytes and passes true. Refuse false BEFORE any write (no agent/key/audit row).
|
|
96
|
+
if p_pop_verified is not true then
|
|
97
|
+
raise exception 'proof-of-possession was not verified; enrollment refused'
|
|
98
|
+
using errcode = '42501';
|
|
99
|
+
end if;
|
|
100
|
+
|
|
101
|
+
-- (1) Public-key shape gate. The enroll contract is raw Ed25519 (32 bytes -> 43
|
|
102
|
+
-- base64url chars).
|
|
103
|
+
if p_agent_public_key_format is distinct from 'raw_ed25519_base64url' then
|
|
104
|
+
raise exception 'agent public key format must be raw_ed25519_base64url' using errcode = '22023';
|
|
105
|
+
end if;
|
|
106
|
+
if p_agent_public_key is null or p_agent_public_key !~ '^[A-Za-z0-9_-]{43}$' then
|
|
107
|
+
raise exception 'malformed agent public key: expected a 43-char base64url raw Ed25519 key'
|
|
108
|
+
using errcode = '22023';
|
|
109
|
+
end if;
|
|
110
|
+
|
|
111
|
+
-- (1a) B2 — decode base64url -> raw 32 bytes and REJECT any non-canonical encoding.
|
|
112
|
+
-- A 32-byte key's 43-char base64url has 2 don't-care low bits, so up to 4 distinct
|
|
113
|
+
-- strings decode to the same key. Re-encoding the decoded bytes canonically and
|
|
114
|
+
-- requiring string equality collapses those aliases to ONE accepted encoding, so the
|
|
115
|
+
-- fingerprint (over raw bytes, below) is a true per-key identity — one keypair can
|
|
116
|
+
-- never enroll as multiple agents, and revocation isolation holds.
|
|
117
|
+
v_raw_key := decode(translate(p_agent_public_key, '-_', '+/') || '=', 'base64');
|
|
118
|
+
if octet_length(v_raw_key) <> 32 then
|
|
119
|
+
raise exception 'agent public key must decode to exactly 32 raw Ed25519 bytes'
|
|
120
|
+
using errcode = '22023';
|
|
121
|
+
end if;
|
|
122
|
+
v_reencoded := rtrim(translate(encode(v_raw_key, 'base64'), '+/', '-_'), '=');
|
|
123
|
+
if v_reencoded is distinct from p_agent_public_key then
|
|
124
|
+
raise exception 'agent public key is not canonical base64url (aliased encoding rejected)'
|
|
125
|
+
using errcode = '22023';
|
|
126
|
+
end if;
|
|
127
|
+
|
|
128
|
+
-- (1b) Reject the all-zero small-order placeholder over the RAW bytes (not a
|
|
129
|
+
-- string-exact match), so no alternate encoding can smuggle it past.
|
|
130
|
+
if v_raw_key = decode(repeat('00', 32), 'hex') then
|
|
131
|
+
raise exception 'agent public key is the all-zero small-order placeholder and is rejected'
|
|
132
|
+
using errcode = '22023';
|
|
133
|
+
end if;
|
|
134
|
+
|
|
135
|
+
-- (2) Recompute-and-assert the fingerprint in-DB over the RAW 32 bytes
|
|
136
|
+
-- (byte-identical to the EF/WebCrypto raw-byte derivation and the client child_keygen
|
|
137
|
+
-- keyRef). A tampered fingerprint -> raise.
|
|
138
|
+
v_fingerprint := 'sha256:' || rtrim(
|
|
139
|
+
translate(encode(extensions.digest(v_raw_key, 'sha256'), 'base64'), '+/', '-_'),
|
|
140
|
+
'=');
|
|
141
|
+
if p_agent_public_key_fingerprint is distinct from v_fingerprint then
|
|
142
|
+
raise exception 'agent public key fingerprint mismatch (recompute != supplied)'
|
|
143
|
+
using errcode = '22023';
|
|
144
|
+
end if;
|
|
145
|
+
|
|
146
|
+
-- (3) act.sub is audit-only, but when present it must be a real value.
|
|
147
|
+
if p_actor_sub is not null and length(trim(p_actor_sub)) = 0 then
|
|
148
|
+
raise exception 'actor sub, when present, must be non-empty' using errcode = '22023';
|
|
149
|
+
end if;
|
|
150
|
+
|
|
151
|
+
v_slug := agent_board._normalize_provision_slug(p_slug);
|
|
152
|
+
v_kind := lower(coalesce(trim(p_agent_kind), ''));
|
|
153
|
+
|
|
154
|
+
-- (4) allowlist lookup by (issuer, audience); must exist + supported + policy-approved.
|
|
155
|
+
select *
|
|
156
|
+
into v_provider
|
|
157
|
+
from agent_board.identity_providers
|
|
158
|
+
where issuer = p_issuer
|
|
159
|
+
and audience = p_audience
|
|
160
|
+
limit 1;
|
|
161
|
+
if v_provider.id is null then
|
|
162
|
+
raise exception 'issuer/audience not in the identity-provider allowlist' using errcode = '42501';
|
|
163
|
+
end if;
|
|
164
|
+
if not v_provider.supported then
|
|
165
|
+
raise exception 'identity provider is not supported' using errcode = '42501';
|
|
166
|
+
end if;
|
|
167
|
+
if not v_provider.policy_approved then
|
|
168
|
+
raise exception 'identity provider is not policy-approved' using errcode = '42501';
|
|
169
|
+
end if;
|
|
170
|
+
|
|
171
|
+
-- (5) scope-ceiling + kind gate. 'system' is never federatable.
|
|
172
|
+
if v_kind = 'system' then
|
|
173
|
+
raise exception 'the system kind cannot be federated' using errcode = '42501';
|
|
174
|
+
end if;
|
|
175
|
+
if v_kind not in ('human', 'agent') then
|
|
176
|
+
raise exception 'agent kind must be human or agent' using errcode = '22023';
|
|
177
|
+
end if;
|
|
178
|
+
if array_length(v_provider.allowed_agent_kinds, 1) is not null
|
|
179
|
+
and not (v_kind = any (v_provider.allowed_agent_kinds)) then
|
|
180
|
+
raise exception 'agent kind % is outside the provider ceiling', v_kind using errcode = '42501';
|
|
181
|
+
end if;
|
|
182
|
+
if not (v_requested_scopes <@ v_provider.allowed_scopes) then
|
|
183
|
+
raise exception 'requested scopes exceed the provider allowed_scopes ceiling' using errcode = '42501';
|
|
184
|
+
end if;
|
|
185
|
+
if not (v_requested_board_scopes <@ v_provider.allowed_board_scopes) then
|
|
186
|
+
raise exception 'requested board scopes exceed the provider allowed_board_scopes ceiling' using errcode = '42501';
|
|
187
|
+
end if;
|
|
188
|
+
|
|
189
|
+
-- (6) dedup on the enrolled key: a fingerprint already enrolled cannot be re-bound.
|
|
190
|
+
-- Pre-check for a clean error; the UNIQUE index is the hard guarantee.
|
|
191
|
+
if exists (select 1 from agent_board.agent_signing_keys k where k.fingerprint = v_fingerprint) then
|
|
192
|
+
raise exception 'this signing key is already enrolled (duplicate fingerprint)'
|
|
193
|
+
using errcode = '23505';
|
|
194
|
+
end if;
|
|
195
|
+
|
|
196
|
+
-- (7) HUMAN OWNER — established idempotently per (issuer, sub) so re-enrollment of the
|
|
197
|
+
-- same human converges to ONE owner auth.users row (recorded as owner_user_id).
|
|
198
|
+
v_owner_user_id := agent_board._ensure_agent_auth_user(
|
|
199
|
+
'fed-human-' || substr(encode(extensions.digest(convert_to(p_issuer || '|' || p_subject, 'UTF8'), 'sha256'), 'hex'), 1, 32));
|
|
200
|
+
|
|
201
|
+
-- (8) AGENT handle — unique per enrolled key. Use the slug; on collision (a human's
|
|
202
|
+
-- 2nd agent under the same slug, or an unrelated handle) disambiguate with a
|
|
203
|
+
-- deterministic per-key hex fragment so a distinct agent row is always created. The
|
|
204
|
+
-- resulting handle is RETURNED so the client can mint /session against it.
|
|
205
|
+
v_key_hex := encode(extensions.digest(v_raw_key, 'sha256'), 'hex');
|
|
206
|
+
v_agent_handle := v_slug;
|
|
207
|
+
if exists (select 1 from agent_board.agents a where a.handle = v_agent_handle) then
|
|
208
|
+
v_agent_handle := v_slug || '-' || substr(v_key_hex, 1, 12);
|
|
209
|
+
end if;
|
|
210
|
+
|
|
211
|
+
-- (9) the agent's OWN board principal (session-usable via its enrolled key).
|
|
212
|
+
v_auth_user_id := agent_board._ensure_agent_auth_user(v_agent_handle);
|
|
213
|
+
|
|
214
|
+
-- (10) create the SEPARATE agent row (never keyed on sub; owner = the human).
|
|
215
|
+
insert into agent_board.agents(
|
|
216
|
+
agent_type,
|
|
217
|
+
kind,
|
|
218
|
+
handle,
|
|
219
|
+
display_name,
|
|
220
|
+
status,
|
|
221
|
+
auth_user_id,
|
|
222
|
+
owner_user_id,
|
|
223
|
+
metadata
|
|
224
|
+
)
|
|
225
|
+
values (
|
|
226
|
+
v_kind,
|
|
227
|
+
v_kind::agent_board.agent_kind,
|
|
228
|
+
v_agent_handle,
|
|
229
|
+
agent_board._default_provision_display_name(v_agent_handle),
|
|
230
|
+
'active',
|
|
231
|
+
v_auth_user_id,
|
|
232
|
+
v_owner_user_id,
|
|
233
|
+
jsonb_build_object(
|
|
234
|
+
'federation', jsonb_build_object(
|
|
235
|
+
'issuer', p_issuer,
|
|
236
|
+
'audience', p_audience,
|
|
237
|
+
'subject', p_subject,
|
|
238
|
+
'actorSub', p_actor_sub,
|
|
239
|
+
'providerId', v_provider.id,
|
|
240
|
+
'signingKeyFingerprint', v_fingerprint,
|
|
241
|
+
'requestedScopes', to_jsonb(v_requested_scopes),
|
|
242
|
+
'requestedBoardScopes', to_jsonb(v_requested_board_scopes),
|
|
243
|
+
'popVerified', true,
|
|
244
|
+
'enrolledAt', now()
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
returning id into v_agent_id;
|
|
249
|
+
|
|
250
|
+
-- (11) enroll the public key as active + proven (satisfies active_requires_proof).
|
|
251
|
+
insert into agent_board.agent_signing_keys(
|
|
252
|
+
id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint,
|
|
253
|
+
status, proof_state, metadata
|
|
254
|
+
)
|
|
255
|
+
values (
|
|
256
|
+
gen_random_uuid(), v_agent_id, v_key_id, v_key_version, p_agent_public_key,
|
|
257
|
+
p_agent_public_key_format, v_fingerprint,
|
|
258
|
+
'active', 'proven',
|
|
259
|
+
jsonb_build_object('enrolledVia', 'federated_identity', 'popVerified', true)
|
|
260
|
+
)
|
|
261
|
+
returning id into v_signing_key_id;
|
|
262
|
+
|
|
263
|
+
-- (12) one static claim-snapshot audit row (the verified claim contains act.sub).
|
|
264
|
+
insert into agent_board.enrollment_federation_audit(
|
|
265
|
+
issuer, audience, subject, human_identity_claim, provider_id,
|
|
266
|
+
agent_id, auth_user_id, requested_scopes, granted_scopes
|
|
267
|
+
)
|
|
268
|
+
values (
|
|
269
|
+
p_issuer, p_audience, p_subject, p_human_identity_claim, v_provider.id,
|
|
270
|
+
v_agent_id, v_auth_user_id, v_requested_scopes, v_requested_scopes
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
-- Server-authoritative identity: the client mints /session with the RETURNED
|
|
274
|
+
-- key_id/key_version/agent_handle (its private key stays in the keystore).
|
|
275
|
+
return query
|
|
276
|
+
select v_agent_id, v_auth_user_id, v_provider.id, v_signing_key_id, v_requested_scopes,
|
|
277
|
+
v_key_id, v_key_version, v_agent_handle;
|
|
278
|
+
end;
|
|
279
|
+
$$;
|
|
280
|
+
|
|
281
|
+
comment on function agent_board.enroll_federated_identity(
|
|
282
|
+
text, text, text, jsonb, text, text, text, text, boolean, text, text, text[], text[]) is
|
|
283
|
+
'PoP-at-enrollment federated identity: re-gates p_pop_verified + allowlist/policy/scope, '
|
|
284
|
+
'decodes the key to raw bytes + rejects non-canonical/all-zero encodings, recomputes-'
|
|
285
|
+
'and-asserts the sha256:base64url fingerprint over the RAW 32 bytes, establishes ONE '
|
|
286
|
+
'human owner (owner_user_id) idempotently per (issuer,sub), creates a SEPARATE '
|
|
287
|
+
'key-identified agent (own auth_user_id) per enrolled key, enrolls the key active+proven, '
|
|
288
|
+
'and RETURNS the server-assigned key_id/key_version/agent_handle so the client mints '
|
|
289
|
+
'/session against the authoritative identity. service_role-only; never returns a Portal token.';
|
|
290
|
+
|
|
291
|
+
revoke all on function agent_board.enroll_federated_identity(
|
|
292
|
+
text, text, text, jsonb, text, text, text, text, boolean, text, text, text[], text[])
|
|
293
|
+
from public, anon, authenticated;
|
|
294
|
+
grant execute on function agent_board.enroll_federated_identity(
|
|
295
|
+
text, text, text, jsonb, text, text, text, text, boolean, text, text, text[], text[])
|
|
296
|
+
to service_role;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
-- MBPORTAL PR #32 reconcile (S1 + S2): bound + isolate agent_board.enroll_rate_events.
|
|
2
|
+
--
|
|
3
|
+
-- S1 — the /enroll rate-cap table (20260719000300) had NO reaper: every under-cap
|
|
4
|
+
-- request INSERTs a row and nothing ever deletes it, so the table grows unbounded
|
|
5
|
+
-- (the challenge path got a reaper; this one did not). FIX: ship
|
|
6
|
+
-- reap_expired_enroll_rate_events(interval), mirroring
|
|
7
|
+
-- reap_expired_signing_key_challenges — security definer, service_role-only,
|
|
8
|
+
-- deletes-and-counts rows older than p_older_than (default 1 hour; comfortably past
|
|
9
|
+
-- any sane rolling window). Scheduling (pg_cron / external cadence) is a deferred
|
|
10
|
+
-- operational owner, exactly like the challenge reaper; the callable function is
|
|
11
|
+
-- in-scope. (The XFF-hop half of S1 is fixed in the /enroll EF, which owns index.ts.)
|
|
12
|
+
--
|
|
13
|
+
-- S2 — enroll_rate_events was created WITHOUT row level security. It grants to
|
|
14
|
+
-- service_role only (unreachable by client JWTs today), but an RLS-disabled table
|
|
15
|
+
-- leaves no defense if a future blanket grant is added. FIX (matching the
|
|
16
|
+
-- workload_providers corrective, 20260714000200): enable RLS + revoke from
|
|
17
|
+
-- anon/authenticated + explicit deny policies, so a client JWT can never read the
|
|
18
|
+
-- per-source rate ledger even under a future grant. service_role bypasses RLS, so the
|
|
19
|
+
-- /enroll cap path is unaffected.
|
|
20
|
+
--
|
|
21
|
+
-- Additive, forward-only. Idempotent. Writes no schema_version (stays 8).
|
|
22
|
+
|
|
23
|
+
-- S1 — reaper.
|
|
24
|
+
create or replace function agent_board.reap_expired_enroll_rate_events(
|
|
25
|
+
p_older_than interval default interval '1 hour'
|
|
26
|
+
)
|
|
27
|
+
returns bigint
|
|
28
|
+
language plpgsql
|
|
29
|
+
security definer
|
|
30
|
+
set search_path = agent_board, public
|
|
31
|
+
as $$
|
|
32
|
+
declare
|
|
33
|
+
v_count bigint;
|
|
34
|
+
begin
|
|
35
|
+
with deleted as (
|
|
36
|
+
delete from agent_board.enroll_rate_events e
|
|
37
|
+
where e.created_at < now() - coalesce(p_older_than, interval '1 hour')
|
|
38
|
+
returning 1
|
|
39
|
+
)
|
|
40
|
+
select count(*) into v_count from deleted;
|
|
41
|
+
return v_count;
|
|
42
|
+
end;
|
|
43
|
+
$$;
|
|
44
|
+
|
|
45
|
+
comment on function agent_board.reap_expired_enroll_rate_events(interval) is
|
|
46
|
+
'Deletes agent_board.enroll_rate_events rows older than p_older_than (default 1 hour); returns the affected count. service_role-only. Scheduling (pg_cron / external cadence) is a deferred operational owner.';
|
|
47
|
+
|
|
48
|
+
revoke all on function agent_board.reap_expired_enroll_rate_events(interval) from public, anon, authenticated;
|
|
49
|
+
grant execute on function agent_board.reap_expired_enroll_rate_events(interval) to service_role;
|
|
50
|
+
|
|
51
|
+
-- S2 — RLS + client isolation (defense-in-depth vs a future blanket grant).
|
|
52
|
+
alter table agent_board.enroll_rate_events enable row level security;
|
|
53
|
+
|
|
54
|
+
revoke all on agent_board.enroll_rate_events from anon, authenticated;
|
|
55
|
+
|
|
56
|
+
drop policy if exists enroll_rate_events_deny_anon on agent_board.enroll_rate_events;
|
|
57
|
+
create policy enroll_rate_events_deny_anon on agent_board.enroll_rate_events
|
|
58
|
+
as permissive for select to anon using (false);
|
|
59
|
+
|
|
60
|
+
drop policy if exists enroll_rate_events_deny_authenticated on agent_board.enroll_rate_events;
|
|
61
|
+
create policy enroll_rate_events_deny_authenticated on agent_board.enroll_rate_events
|
|
62
|
+
as permissive for select to authenticated using (false);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@consiliency/agent-board-schema",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Canonical, immutable, exact-pinnable agent_board schema bundle: ordered SQL migrations, digest + compatibility manifest, and deterministic materialize/plan/check tooling for adopters.",
|
|
5
5
|
"repository": "https://github.com/Consiliency/message-board.git",
|
|
6
6
|
"type": "module",
|
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
"LICENSE",
|
|
26
26
|
"NOTICE"
|
|
27
27
|
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build:manifest": "node ../../scripts/build-schema-manifest.mjs",
|
|
30
|
+
"check:manifest": "node ../../scripts/build-schema-manifest.mjs --check",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"pack:smoke": "node ../../scripts/schema-packed-consumer-smoke.mjs"
|
|
33
|
+
},
|
|
28
34
|
"publishConfig": {
|
|
29
35
|
"access": "public",
|
|
30
36
|
"registry": "https://registry.npmjs.org"
|
|
@@ -32,11 +38,5 @@
|
|
|
32
38
|
"engines": {
|
|
33
39
|
"node": ">=18"
|
|
34
40
|
},
|
|
35
|
-
"license": "Apache-2.0"
|
|
36
|
-
|
|
37
|
-
"build:manifest": "node ../../scripts/build-schema-manifest.mjs",
|
|
38
|
-
"check:manifest": "node ../../scripts/build-schema-manifest.mjs --check",
|
|
39
|
-
"test": "vitest run",
|
|
40
|
-
"pack:smoke": "node ../../scripts/schema-packed-consumer-smoke.mjs"
|
|
41
|
-
}
|
|
42
|
-
}
|
|
41
|
+
"license": "Apache-2.0"
|
|
42
|
+
}
|