@multiagency/sdk 0.3.1
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/CHANGES.md +21 -0
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/THIRD-PARTY-NOTICES +127 -0
- package/bin/release-server.d.mts +29 -0
- package/bin/release-server.mjs +143 -0
- package/dist/compensation-contract.d.ts +370 -0
- package/dist/compensation-contract.js +51 -0
- package/dist/contract/agent-connection.d.ts +86 -0
- package/dist/contract/agent-connection.js +36 -0
- package/dist/contract/app-access.d.ts +141 -0
- package/dist/contract/app-access.js +57 -0
- package/dist/contract/app-client.d.ts +140 -0
- package/dist/contract/app-client.js +61 -0
- package/dist/contract/app-consent.d.ts +68 -0
- package/dist/contract/app-consent.js +91 -0
- package/dist/contract/assets.d.ts +6 -0
- package/dist/contract/assets.js +6 -0
- package/dist/contract/client.d.ts +347 -0
- package/dist/contract/client.js +81 -0
- package/dist/contract/gateway.d.ts +74 -0
- package/dist/contract/gateway.js +47 -0
- package/dist/contract/index.d.ts +26 -0
- package/dist/contract/index.js +25 -0
- package/dist/contract/inputs.d.ts +230 -0
- package/dist/contract/inputs.js +99 -0
- package/dist/contract/intents-account.d.ts +5 -0
- package/dist/contract/intents-account.js +8 -0
- package/dist/contract/orpc-client.js +2037 -0
- package/dist/contract/owner-capabilities.d.ts +81 -0
- package/dist/contract/owner-capabilities.js +33 -0
- package/dist/contract/owner-client.d.ts +99 -0
- package/dist/contract/owner-client.js +146 -0
- package/dist/contract/owner-contract.d.ts +1156 -0
- package/dist/contract/owner-contract.js +37 -0
- package/dist/contract/owner-inputs.d.ts +166 -0
- package/dist/contract/owner-inputs.js +1 -0
- package/dist/contract/owner-outputs.d.ts +5749 -0
- package/dist/contract/owner-outputs.js +751 -0
- package/dist/contract/package-version.d.ts +5 -0
- package/dist/contract/package-version.js +2 -0
- package/dist/contract/payee-entry.d.ts +10 -0
- package/dist/contract/payee-entry.js +10 -0
- package/dist/contract/payee.d.ts +326 -0
- package/dist/contract/payee.js +32 -0
- package/dist/contract/permissions.d.ts +20 -0
- package/dist/contract/permissions.js +64 -0
- package/dist/contract/proposals.d.ts +178 -0
- package/dist/contract/proposals.js +27 -0
- package/dist/contract/run-log-schema.d.ts +888 -0
- package/dist/contract/run-log-schema.js +150 -0
- package/dist/contract/run-progress-schema.d.ts +104 -0
- package/dist/contract/run-progress-schema.js +20 -0
- package/dist/payee-contract.d.ts +9 -0
- package/dist/payee-contract.js +15 -0
- package/package.json +58 -0
- package/provenance.json +61 -0
- package/schemas/agentConnection.json +76 -0
- package/schemas/agentConnectionIssue.json +22 -0
- package/schemas/agentConnectionList.json +88 -0
- package/schemas/agentConnectionStarted.json +90 -0
- package/schemas/compensationSend.json +40 -0
- package/schemas/errorBody.json +16 -0
- package/schemas/fileResult.json +67 -0
- package/schemas/invoicesView.json +130 -0
- package/schemas/ownerCompensationView.json +556 -0
- package/schemas/payeeCompensationView.json +546 -0
- package/schemas/payeeWorkOrderRequest.json +31 -0
- package/schemas/publicWorkOrder.json +94 -0
- package/schemas/reportCommand.json +82 -0
- package/schemas/requestRecord.json +149 -0
- package/schemas/session.json +20 -0
- package/schemas/workOrderList.json +43 -0
- package/schemas/workOrderRequestResult.json +20 -0
- package/skills/app-server/SKILL.md +190 -0
- package/skills/getting-started/SKILL.md +90 -0
- package/skills/owner-workflows/SKILL.md +157 -0
- package/skills/payee-workflows/SKILL.md +200 -0
package/CHANGES.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @multiagency/sdk changes
|
|
2
|
+
|
|
3
|
+
Every SDK change appends a note under the next version's heading when it is made: what changed for the apps, and any
|
|
4
|
+
wire change all apps must update for together. The first change after a release moves `package-version.ts` to the next
|
|
5
|
+
version, so a package staged or packed from changed sources never carries a released version's number. The release
|
|
6
|
+
candidate dates the heading. Under 0.x, any public-interface change bumps at least the minor version.
|
|
7
|
+
|
|
8
|
+
## 0.3.1 (2026-10-31)
|
|
9
|
+
|
|
10
|
+
No code, type or wire change.
|
|
11
|
+
|
|
12
|
+
- **License: MIT** (copyright MultiAgency LLC).
|
|
13
|
+
- **README:** "What the platform does for your product": what the provider does, what the platform guarantees to an
|
|
14
|
+
app and what the app still does, and what has run where for each client's routes.
|
|
15
|
+
- **app-server skill:** the platform's host serves only `/apps/v1` and the confirm page (no sign-in or owner page), and
|
|
16
|
+
setup there is for Payables agents only. A vote the platform can't verify is refused before anything reaches the
|
|
17
|
+
provider (`refused`, code `local_authorization_held`).
|
|
18
|
+
|
|
19
|
+
## Earlier versions
|
|
20
|
+
|
|
21
|
+
0.0.1 to 0.3.0: see the [changelog at sdk-v0.3.0](https://github.com/MultiAgency/platform/blob/sdk-v0.3.0/docs/sdk-changes.md).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MultiAgency LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# MultiAgency SDK
|
|
2
|
+
|
|
3
|
+
Build a product interface or an external agent that uses the MultiAgency platform's existing agreements, contribution reports, payments and Treasury workflows. The SDK supplies clients, response schemas and recovery outcomes. The platform keeps identity, authorization, authoritative records and guarded execution.
|
|
4
|
+
|
|
5
|
+
Start with one supported workflow. A product may provide its own screens, navigation, drafts and preferences while calling the same platform. A new server-side domain or authority model needs a platform extension; importing this package does not create one.
|
|
6
|
+
|
|
7
|
+
## Install the package
|
|
8
|
+
|
|
9
|
+
The SDK is MIT-licensed. Install it with:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
pnpm add @multiagency/sdk zod@^4.6.5
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Each package carries `provenance.json`: the source commit it was packed from, any uncommitted paths in that tree, and a digest for every file it was built from. One version number identifies one package: npm does not let a published version be republished with different contents (per npm's documented policy). The package's own README, skills and `CHANGES.md` describe that exact version.
|
|
16
|
+
|
|
17
|
+
Zod is the one peer: a product's schemas and the SDK's share one Zod. The owner entry (`@multiagency/sdk`) bundles its own `@orpc/client` 2.0.0-beta.47 (licenses in `THIRD-PARTY-NOTICES`) and loads no other oRPC package, so a product on another oRPC version, or a host that shares its own as a Module Federation singleton, keeps it; its bundle then carries both oRPC clients. `@multiagency/sdk/contract` (`ownerContract`, for an app implementing a server from the contract) loads `@orpc/contract` 2.0.0-beta.47, a dependency, as does nothing else. The owner entry also depends on `@ag-ui/core` 1.0.3. The contributor entry loads none of them. The installed package's dependencies and peerDependencies are authoritative for that package. The supported Node target is Node 24. TypeScript consumers use ESM with NodeNext resolution and the ES2023 and DOM libraries for the clients' web request types. The Node-only release-server entry also needs Node typings.
|
|
18
|
+
|
|
19
|
+
For development against unreleased SDK source, install an SDK repository checkout's dependencies and run `pnpm sdk:dev --once` there, then link your product to its generated `packages/sdk` directory. Use `pnpm sdk:dev` to watch changes. The generated directory is not source and is intentionally absent from Git. A linked checkout can differ from the published package of the same version, so record which one a release was built with, and keep local paths out of portable release inputs.
|
|
20
|
+
|
|
21
|
+
## Choose the client and supported access
|
|
22
|
+
|
|
23
|
+
| Product or caller | Client | Required access |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| Owner-facing browser product | `ownerClient` from `@multiagency/sdk` | A host-local owner/member session and access to the configured agent |
|
|
26
|
+
| Contributor-facing browser product | `payeeClient` from `@multiagency/sdk/payee` | The contributor's session on that contributor entry |
|
|
27
|
+
| Contributor's external agent | `payeeClient({ base, token })` | An expiring, revocable report connection started by the contributor |
|
|
28
|
+
| Treasury external agent | `ownerClient` with an absolute machine base and a `fetch` that adds the bearer | An existing wallet-owner-issued Treasury credential with the required scopes; see owner-workflows |
|
|
29
|
+
| An app's server (for example the MultiAgency dashboard) | `appClient({ base, credential })` from `@multiagency/sdk` | The deployment's operator-issued service credential, plus a live owner consent per Organization and agent |
|
|
30
|
+
|
|
31
|
+
The platform operator configures a browser product's host, reachable agents and frontend release. Browser products use that host's gateway and session. A custom frontend does not receive a provider key or grant, and cannot choose another host's agents.
|
|
32
|
+
|
|
33
|
+
An app's server reaches agents under `/apps/v1` and never from a browser: the service credential is a server secret. The
|
|
34
|
+
owner signs `consentMessage(...)` (or `setupMessage(...)` for a hosted agent) with NEP-413 in the app; the server submits
|
|
35
|
+
it. A consent reaches the listed reads (`APP_READS`) and owner actions (`APP_OWNER`) and, for `owner/prepare`, only
|
|
36
|
+
actions the owner can read when signing or that platform code builds entirely (`AppOwnerAction`); everything else is
|
|
37
|
+
refused. The app never asks an owner to sign the platform's sign-in text. See the platform's `docs/agent-access.md`,
|
|
38
|
+
"App access". No production MultiAgency host serves the `/apps/v1` routes yet; the operator runs them on a staging host
|
|
39
|
+
for its own tests. Build against the SDK repository's local stack (`pnpm test:e2e:stack --app <deployment>`), with a
|
|
40
|
+
simulated provider, chain and wallet.
|
|
41
|
+
|
|
42
|
+
An agent connection permits its contributor's reports and compensation reads only. It cannot file invoices, approve payments, sign owner actions or manage connections. Treasury machine access is a separate existing route and credential; Payables owner delegation and DAO machine delegation are unavailable.
|
|
43
|
+
|
|
44
|
+
## What the platform does for your product
|
|
45
|
+
|
|
46
|
+
The NEAR Intents Agent API provider holds the funds, enforces the policy the owner signed (payees, caps, budget, held approvals), prepares the owner actions a wallet or DAO signs, and executes money actions. The platform does the rest of what a product would otherwise build around the provider, and no client of this SDK receives a provider key or grant.
|
|
47
|
+
|
|
48
|
+
What your product can rely on, and what it still does:
|
|
49
|
+
|
|
50
|
+
- **Each money action is sent at most once.** One executor per agent sends every action from one place, after a write-ahead record, so a crash, restart or lost reply never sends it again, and an app's `requestId` is honored once. An outcome the platform can't prove stays unresolved and blocks new money actions on that agent. Your product never resends after `unknown`; it reads `activity` and `unresolved` (see Handle outcomes explicitly).
|
|
51
|
+
- **What the provider can't check is refused before sending.** The provider enforces the signed policy; the platform also checks that the policy is still the one in force, the grant fits the action, a swap buys an asset the policy lists, a held payout isn't duplicated and a saved plan runs as it was quoted. A recipient that doesn't exist on NEAR is refused before anything is recorded. Your product branches on the refusal's `kind` and code, never its text.
|
|
52
|
+
- **Owners approve what they read.** Owner actions are prepared on the platform and completed only by the owner's own signature or a DAO's vote. A vote on a held payout is checked against the provider's own record of that payout, so the approval releases exactly the terms shown. Your product shows what the owner is signing; it never signs for them.
|
|
53
|
+
- **Callers are bound to their host, owner and agent.** Sessions are host-local; machine credentials and app consents name one host, agent and owner, expire, and can be revoked. Your product keeps credentials server-side and out of logs and URLs.
|
|
54
|
+
|
|
55
|
+
What has run where, by the routes each client calls:
|
|
56
|
+
|
|
57
|
+
| Client | Status |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `ownerClient`, in the owner's session | Mainnet-observed on the hosted platform: reads, owner actions, held-payout votes, and plan execution through the Treasury autopilot |
|
|
60
|
+
| `ownerClient` with a machine credential | Offline- and stack-tested; not observed on mainnet |
|
|
61
|
+
| `appClient` | Mainnet-observed on platform staging (2026-10-28, 2026-10-30); not in production; wallet-owned agents only |
|
|
62
|
+
| `payeeClient`, in the contributor's session | Mainnet-observed on the hosted invoice entry: invoices and agreement requests (2026-10-12, 2026-10-14) |
|
|
63
|
+
| `payeeClient` with a report connection | Offline- and stack-tested; not deployed |
|
|
64
|
+
|
|
65
|
+
Crash recovery and the refusals above are tested offline, not observed on mainnet. The platform's [mainnet log](https://github.com/MultiAgency/platform/blob/main/docs/mainnet-log.md) records what has run on mainnet, and [enforcement](https://github.com/MultiAgency/platform/blob/main/docs/enforcement.md) what the signed policy guarantees.
|
|
66
|
+
|
|
67
|
+
## First read from a browser product
|
|
68
|
+
|
|
69
|
+
After signing in on the product's configured host, use the configured agent key:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { ownerClient } from "@multiagency/sdk";
|
|
73
|
+
|
|
74
|
+
const owner = ownerClient({ base: "/a/payout" });
|
|
75
|
+
const overview = await owner.call("overview");
|
|
76
|
+
|
|
77
|
+
if (overview.ok) {
|
|
78
|
+
console.dir(overview.value);
|
|
79
|
+
} else {
|
|
80
|
+
console.log(overview.kind);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Replace `payout` with the key configured for your host. Use the typed response to render your screen. A `session` result needs sign-in; `forbidden` means this identity lacks access. Capability and permission helpers can guide what to display; the server authorizes every request.
|
|
85
|
+
|
|
86
|
+
The SDK repository's `examples/minimal-prototype` is the browser reference: a page bundled with rsbuild that imports `@multiagency/sdk`, signs in through the gateway, reads its agent and takes one wallet-signed owner action, keeping the prepared action's `correlationId` across a lost reply and a reload. Its end-to-end test runs it on the isolated stack with a simulated provider. Use the public clients rather than reproducing the RPC wire format.
|
|
87
|
+
|
|
88
|
+
## First read from a contributor agent
|
|
89
|
+
|
|
90
|
+
Have the contributor start a report connection from their signed-in application. Give the agent the connection token and the API entry base: the origin, or the origin plus `/i/<instance>`. Do not use a page URL ending in `/submit` or `/report` as the API base.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { payeeClient } from "@multiagency/sdk/payee";
|
|
94
|
+
|
|
95
|
+
const entry = process.env.MULTIAGENCY_ENTRY;
|
|
96
|
+
const token = process.env.MULTIAGENCY_REPORT_TOKEN;
|
|
97
|
+
if (!entry || !token) throw new Error("Provide the contributor API entry and report token.");
|
|
98
|
+
if (new URL(entry).protocol !== "https:") throw new Error("Use the HTTPS contributor entry.");
|
|
99
|
+
|
|
100
|
+
const contributor = payeeClient({ base: entry.replace(/\/$/, ""), token });
|
|
101
|
+
const session = await contributor.compensationSession();
|
|
102
|
+
if (!session.ok) throw new Error(`Contributor access: ${session.kind}`);
|
|
103
|
+
|
|
104
|
+
const records = await contributor.compensation();
|
|
105
|
+
if (records.ok) {
|
|
106
|
+
console.log(records.value.agreements.map(a => ({
|
|
107
|
+
id: a.id, title: a.terms.title, remainingAtomicUsdc: a.remaining,
|
|
108
|
+
})));
|
|
109
|
+
} else {
|
|
110
|
+
console.log(records.kind);
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The token is sent as a bearer without browser cookies. Keep it out of source, URLs and logs. An agent never needs the contributor's wallet key or an exported browser cookie.
|
|
115
|
+
|
|
116
|
+
## Build the report workflow
|
|
117
|
+
|
|
118
|
+
1. Read and select the intended agreement. Its scope, period and remaining allowance bound the work; allowance is not an earned amount.
|
|
119
|
+
2. Construct the complete report command using the payee-workflows example. Save its request ID, command ID and exact contents before sending.
|
|
120
|
+
3. Use `expectedVersion: 0` for a new request. For a revision, keep the request ID and use its current returned version with a new command ID for the new intended change.
|
|
121
|
+
4. `submit: false` saves a server draft; `submit: true` sends the report to owner review. Only a draft or a request returned for changes can be revised.
|
|
122
|
+
5. An optional report amount is a proposal in atomic USDC: `"12000000"` is 12 USDC. The owner chooses a separate approved amount.
|
|
123
|
+
6. After `unknown`, keep the exact command and read the original request by its ID. Recovery is a read. Do not create a replacement or resend automatically. The entry records each `commandId` with its content: sending the identical command again returns the original record, and a different payload under the same `commandId` is refused with `command_conflict`. A missing request after `unknown` does not prove that nothing was recorded.
|
|
124
|
+
7. Match the returned payment by request ID. An approved report is not a paid payment: only a payment whose status is `paid` reports recorded settlement. Display the last recorded status as read.
|
|
125
|
+
|
|
126
|
+
Refusals carry a `code`, for example `stale_request` (the version moved; read it again), `request_locked` (already submitted, approved or declined), `outside_period` (the report period must fit the agreement), `agreement_changed` (a request keeps its first agreement), `not_found`, `intake_full`, `identity_conflict` (the request's `id` is already used), `command_conflict` and `rate_limited`.
|
|
127
|
+
|
|
128
|
+
Validate fields with `reportCommand.safeParse` before sending. Invalid structured input throws a Zod validation error locally and sends nothing.
|
|
129
|
+
|
|
130
|
+
## Handle outcomes explicitly
|
|
131
|
+
|
|
132
|
+
Reads may be repeated after `unavailable`. `session` and `forbidden` require different user actions. A typed refusal says the action did not proceed; correct the stated cause before another intended action. An `unknown` action may have taken effect. Preserve its available identity and follow the documented reads.
|
|
133
|
+
|
|
134
|
+
The SDK never automatically resubmits an action. Some owner procedures have no caller-held lookup identity; the owner-workflows skill describes their limits. Diagnostic provider request IDs are not verified operation links. An unresolved payment may continue blocking its agent until a supported resolution exists.
|
|
135
|
+
|
|
136
|
+
## Continue with the bundled guidance
|
|
137
|
+
|
|
138
|
+
The package contains `skills/getting-started/SKILL.md`, `skills/payee-workflows/SKILL.md`, `skills/owner-workflows/SKILL.md` and `skills/app-server/SKILL.md`. These files describe the clients in the installed package and can be read directly or through TanStack Intent. `CHANGES.md` records consumer-impacting changes.
|
|
139
|
+
|
|
140
|
+
Use `@multiagency/sdk/schemas/*` for the published JSON schemas. The Node-only `@multiagency/sdk/release-server` serves immutable frontend artifacts; it does not authenticate callers or run a financial executor.
|
|
141
|
+
|
|
142
|
+
For a local rehearsal, the SDK repository's existing simulated stack supplies the gateway and provider fixtures. Keep simulated sign-in, approvals, funding and settlement visibly separate from the product's SDK calls. A successful simulation does not authorize a hosted deployment or financial action.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
@orpc/client@2.0.0-beta.47 (MIT), bundled in dist/contract/orpc-client.js
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2023 oRPC
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
|
24
|
+
|
|
25
|
+
------------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
@orpc/shared@2.0.0-beta.47 (MIT), bundled in dist/contract/orpc-client.js
|
|
28
|
+
|
|
29
|
+
MIT License
|
|
30
|
+
|
|
31
|
+
Copyright (c) 2023 oRPC
|
|
32
|
+
|
|
33
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
34
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
35
|
+
in the Software without restriction, including without limitation the rights
|
|
36
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
37
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
38
|
+
furnished to do so, subject to the following conditions:
|
|
39
|
+
|
|
40
|
+
The above copyright notice and this permission notice shall be included in all
|
|
41
|
+
copies or substantial portions of the Software.
|
|
42
|
+
|
|
43
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
44
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
45
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
46
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
47
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
48
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
49
|
+
SOFTWARE.
|
|
50
|
+
|
|
51
|
+
------------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
@standard-server/core@0.13.0 (MIT), bundled in dist/contract/orpc-client.js
|
|
54
|
+
|
|
55
|
+
MIT License
|
|
56
|
+
|
|
57
|
+
Copyright (c) 2025 Standard Server
|
|
58
|
+
|
|
59
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
60
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
61
|
+
in the Software without restriction, including without limitation the rights
|
|
62
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
63
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
64
|
+
furnished to do so, subject to the following conditions:
|
|
65
|
+
|
|
66
|
+
The above copyright notice and this permission notice shall be included in all
|
|
67
|
+
copies or substantial portions of the Software.
|
|
68
|
+
|
|
69
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
70
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
71
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
72
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
73
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
74
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
75
|
+
SOFTWARE.
|
|
76
|
+
|
|
77
|
+
------------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
@standard-server/fetch@0.13.0 (MIT), bundled in dist/contract/orpc-client.js
|
|
80
|
+
|
|
81
|
+
MIT License
|
|
82
|
+
|
|
83
|
+
Copyright (c) 2025 Standard Server
|
|
84
|
+
|
|
85
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
86
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
87
|
+
in the Software without restriction, including without limitation the rights
|
|
88
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
89
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
90
|
+
furnished to do so, subject to the following conditions:
|
|
91
|
+
|
|
92
|
+
The above copyright notice and this permission notice shall be included in all
|
|
93
|
+
copies or substantial portions of the Software.
|
|
94
|
+
|
|
95
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
96
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
97
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
98
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
99
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
100
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
101
|
+
SOFTWARE.
|
|
102
|
+
|
|
103
|
+
------------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
@standard-server/shared@0.13.0 (MIT), bundled in dist/contract/orpc-client.js
|
|
106
|
+
|
|
107
|
+
MIT License
|
|
108
|
+
|
|
109
|
+
Copyright (c) 2025 Standard Server
|
|
110
|
+
|
|
111
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
112
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
113
|
+
in the Software without restriction, including without limitation the rights
|
|
114
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
115
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
116
|
+
furnished to do so, subject to the following conditions:
|
|
117
|
+
|
|
118
|
+
The above copyright notice and this permission notice shall be included in all
|
|
119
|
+
copies or substantial portions of the Software.
|
|
120
|
+
|
|
121
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
122
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
123
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
124
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
125
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
126
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
127
|
+
SOFTWARE.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Types for release-server.mjs (shipped as `@multiagency/sdk/release-server` and the `multiagency-release-server` bin).
|
|
2
|
+
import type { Server } from "node:http";
|
|
3
|
+
|
|
4
|
+
/** A release's `contract.json`, as the server reads it. Unknown keys are kept. */
|
|
5
|
+
export type ReleaseContract = {
|
|
6
|
+
version: 1;
|
|
7
|
+
product: string;
|
|
8
|
+
required: string[];
|
|
9
|
+
optional?: string[];
|
|
10
|
+
/** Logical path → file in the release, for example `{ "/": "index.html", "/skill.md": "skill.md" }`. */
|
|
11
|
+
entries: Record<string, string>;
|
|
12
|
+
fallback?: string;
|
|
13
|
+
strip?: string;
|
|
14
|
+
assetPrefix?: string;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
};
|
|
17
|
+
/** A complete release on disk: its id (the directory digest), directory and contract. */
|
|
18
|
+
export type Release = { id: string; dir: string; contract: ReleaseContract };
|
|
19
|
+
|
|
20
|
+
/** The release id of a directory: sha256 over its sorted relative names and bytes. */
|
|
21
|
+
export function digest(dir: string): string;
|
|
22
|
+
/** The complete release `id` under `root`. Throws when the id, contract or any declared entry is invalid. */
|
|
23
|
+
export function readRelease(root: string, id: string): Release;
|
|
24
|
+
/** Copies a complete build under `root` as its digest and returns that id; an interrupted stage never appears. */
|
|
25
|
+
export function stage(root: string, built: string): string;
|
|
26
|
+
/** Verifies release `id` (bytes and retained hashed assets) and switches `ACTIVE` to it atomically. */
|
|
27
|
+
export function activate(root: string, id: string): Release;
|
|
28
|
+
/** An HTTP server for the active release under `root`: GET/HEAD only, presentation only. Not yet listening. */
|
|
29
|
+
export function releaseServer(root: string): Server;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// One release asset server for app frontends behind the platform gateway (P2 D5). Presentation only:
|
|
3
|
+
// GET/HEAD, no API, no outbound calls, no platform configuration. Environment: HOST, PORT, RELEASES_ROOT.
|
|
4
|
+
// serve (default) serve RELEASES_ROOT/<ACTIVE>/
|
|
5
|
+
// stage <built-dir> copy a complete build to RELEASES_ROOT/<digest>/ and print the id
|
|
6
|
+
// activate <id> verify that release, then switch ACTIVE atomically
|
|
7
|
+
// A release is a directory whose contract.json declares { version, product, required, optional?, entries,
|
|
8
|
+
// fallback?, strip?, assetPrefix? }. Its id is the digest of the directory (the platform E2E runner's digest()).
|
|
9
|
+
import { createServer } from 'node:http';
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { chmodSync, cpSync, existsSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { extname, join, resolve, sep } from 'node:path';
|
|
13
|
+
import { pathToFileURL } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const ID = /^[a-f0-9]{64}$/;
|
|
16
|
+
const HASHED = /^\/static\/[a-zA-Z0-9_./-]*\.[a-f0-9]{8,}\.[a-z0-9]+$/;
|
|
17
|
+
const TYPES = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8',
|
|
18
|
+
'.json': 'application/json', '.md': 'text/markdown; charset=utf-8', '.woff2': 'font/woff2', '.woff': 'font/woff', '.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon' };
|
|
19
|
+
|
|
20
|
+
/** Same algorithm as test/e2e/run.mjs digest(): sorted relative names and bytes. */
|
|
21
|
+
export function digest(dir) {
|
|
22
|
+
const hash = createHash('sha256');
|
|
23
|
+
const scan = (path, prefix = '') => {
|
|
24
|
+
for (const file of readdirSync(path, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
25
|
+
const name = prefix + file.name;
|
|
26
|
+
if (file.isDirectory()) scan(join(path, file.name), name + '/');
|
|
27
|
+
else if (file.isFile()) hash.update(name).update('\0').update(readFileSync(join(path, file.name))).update('\0');
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
scan(dir); return hash.digest('hex');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Real path inside the root, or null: no `..`, no symlink escape. */
|
|
34
|
+
function inside(root, ...parts) {
|
|
35
|
+
if (parts.some(p => p.split('/').includes('..'))) return null;
|
|
36
|
+
try {
|
|
37
|
+
const real = realpathSync(join(root, ...parts)), base = realpathSync(root);
|
|
38
|
+
return real === base || real.startsWith(base + sep) ? real : null;
|
|
39
|
+
} catch { return null; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A complete release: valid id, parsable contract, every declared entry present. Throws otherwise. */
|
|
43
|
+
export function readRelease(root, id) {
|
|
44
|
+
if (!ID.test(id)) throw new Error('Invalid release id');
|
|
45
|
+
const dir = inside(root, id);
|
|
46
|
+
if (!dir || !statSync(dir).isDirectory()) throw new Error('Release missing');
|
|
47
|
+
const contract = JSON.parse(readFileSync(join(dir, 'contract.json'), 'utf8'));
|
|
48
|
+
if (contract.version !== 1 || typeof contract.product !== 'string' || !contract.entries || typeof contract.entries !== 'object') throw new Error('Invalid contract');
|
|
49
|
+
for (const file of [...Object.values(contract.entries), ...(contract.fallback ? [contract.fallback] : [])])
|
|
50
|
+
if (typeof file !== 'string' || !inside(dir, file) || !statSync(inside(dir, file)).isFile()) throw new Error(`Release incomplete: ${file}`);
|
|
51
|
+
return { id, dir, contract };
|
|
52
|
+
}
|
|
53
|
+
/** Releases that were activated, oldest first. Only these may supply retained assets; staged or partial ones never do. */
|
|
54
|
+
const history = root => { try { return readFileSync(join(root, 'HISTORY'), 'utf8').split('\n').filter(id => ID.test(id)); } catch { return []; } };
|
|
55
|
+
const activeId = root => { try { return readFileSync(join(root, 'ACTIVE'), 'utf8').trim(); } catch { return null; } };
|
|
56
|
+
|
|
57
|
+
/** Copy into a temporary directory, verify, then rename into place: an interrupted stage never appears as a release. */
|
|
58
|
+
/** A copy keeps the build's modes. A frozen build is read-only, and a read-only directory can be neither renamed nor
|
|
59
|
+
* emptied, so the staged copy's directories are made owner-writable. File contents and the release id are unchanged. */
|
|
60
|
+
function ownerWritable(dir) {
|
|
61
|
+
if (!existsSync(dir)) return;
|
|
62
|
+
chmodSync(dir, statSync(dir).mode | 0o700);
|
|
63
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) if (entry.isDirectory()) ownerWritable(join(dir, entry.name));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function stage(root, built) {
|
|
67
|
+
const id = digest(built), target = join(root, id);
|
|
68
|
+
if (existsSync(target)) { if (digest(target) !== id) throw new Error('An existing release does not match its id'); readRelease(root, id); return id; }
|
|
69
|
+
const temporary = join(root, `.staging-${process.pid}-${Date.now()}`);
|
|
70
|
+
try {
|
|
71
|
+
cpSync(built, temporary, { recursive: true });
|
|
72
|
+
ownerWritable(temporary);
|
|
73
|
+
if (digest(temporary) !== id) throw new Error('Staged copy does not match its build');
|
|
74
|
+
renameSync(temporary, target);
|
|
75
|
+
} finally { ownerWritable(temporary); rmSync(temporary, { recursive: true, force: true }); }
|
|
76
|
+
try { readRelease(root, id); } catch (error) { rmSync(target, { recursive: true, force: true }); throw error; }
|
|
77
|
+
return id;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Verify completeness, identity and that no retained hashed file differs under the same name; then switch. */
|
|
81
|
+
export function activate(root, id) {
|
|
82
|
+
const release = readRelease(root, id);
|
|
83
|
+
if (digest(release.dir) !== id) throw new Error('Release bytes do not match its id');
|
|
84
|
+
const files = dir => { const out = []; const scan = (p, prefix) => { for (const e of readdirSync(p, { withFileTypes: true })) e.isDirectory() ? scan(join(p, e.name), `${prefix}${e.name}/`) : out.push(`${prefix}${e.name}`); }; if (existsSync(dir)) scan(dir, '/static/'); return out; };
|
|
85
|
+
for (const other of history(root).filter(name => name !== id))
|
|
86
|
+
for (const name of files(join(root, id, 'static')).filter(n => HASHED.test(n))) {
|
|
87
|
+
const theirs = join(root, other, name);
|
|
88
|
+
if (existsSync(theirs) && !readFileSync(theirs).equals(readFileSync(join(root, id, name)))) throw new Error(`Conflicting retained asset ${name} in ${other}`);
|
|
89
|
+
}
|
|
90
|
+
const activated = [...history(root).filter(name => name !== id), id];
|
|
91
|
+
writeFileSync(join(root, 'HISTORY.next'), activated.join('\n') + '\n'); renameSync(join(root, 'HISTORY.next'), join(root, 'HISTORY'));
|
|
92
|
+
writeFileSync(join(root, 'ACTIVE.next'), id + '\n'); renameSync(join(root, 'ACTIVE.next'), join(root, 'ACTIVE'));
|
|
93
|
+
return release;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function releaseServer(root) {
|
|
97
|
+
return createServer((req, res) => {
|
|
98
|
+
res.setHeader('x-content-type-options', 'nosniff');
|
|
99
|
+
res.setHeader('referrer-policy', 'no-referrer');
|
|
100
|
+
res.setHeader('x-frame-options', 'DENY');
|
|
101
|
+
res.setHeader('cache-control', 'no-store');
|
|
102
|
+
if (!['GET', 'HEAD'].includes(req.method)) return res.writeHead(405).end();
|
|
103
|
+
let release;
|
|
104
|
+
try { release = readRelease(root, activeId(root) ?? ''); }
|
|
105
|
+
catch { return res.writeHead(503, { 'content-type': 'application/json' }).end('{"error":"No complete active release"}'); }
|
|
106
|
+
res.setHeader('x-release-id', release.id);
|
|
107
|
+
const { contract } = release;
|
|
108
|
+
let path = new URL(req.url, 'http://release').pathname;
|
|
109
|
+
if (contract.strip) path = path.replace(new RegExp(`^(?:${contract.strip})(?=/|$)`), '') || '/';
|
|
110
|
+
if (contract.assetPrefix && path.startsWith(`${contract.assetPrefix}/static/`)) path = path.slice(contract.assetPrefix.length);
|
|
111
|
+
const send = (file, cache) => {
|
|
112
|
+
const bytes = readFileSync(file);
|
|
113
|
+
res.writeHead(200, { 'content-type': TYPES[extname(file)] ?? 'application/octet-stream', ...(cache && { 'cache-control': cache }) }).end(req.method === 'HEAD' ? undefined : bytes);
|
|
114
|
+
};
|
|
115
|
+
if (path === '/health') return res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true, product: contract.product, release: release.id }));
|
|
116
|
+
if (path === '/contract.json') return send(join(release.dir, 'contract.json'));
|
|
117
|
+
const entry = contract.entries[path] ?? contract.entries[path.replace(/\/$/, '')];
|
|
118
|
+
if (entry) return send(inside(release.dir, entry));
|
|
119
|
+
if (HASHED.test(path)) {
|
|
120
|
+
// Hashed names are immutable: the active release first, then retained ones for pages already open.
|
|
121
|
+
for (const id of [release.id, ...history(root).filter(name => name !== release.id).reverse()]) {
|
|
122
|
+
const file = inside(root, id, path.slice(1));
|
|
123
|
+
if (file && statSync(file).isFile()) return send(file, 'public, max-age=31536000, immutable');
|
|
124
|
+
}
|
|
125
|
+
return res.writeHead(404).end('Not found');
|
|
126
|
+
}
|
|
127
|
+
// A declared SPA fallback answers client routes only: never an API-like, static or extension path.
|
|
128
|
+
if (contract.fallback && !extname(path) && !/^\/(api|internal|static|gw|machine)(\/|$)/.test(path)) return send(inside(release.dir, contract.fallback));
|
|
129
|
+
res.writeHead(404).end('Not found');
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The real path: npm's bin link is a symlink, and import.meta.url names its target.
|
|
134
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
135
|
+
const root = process.env.RELEASES_ROOT, [command = 'serve', argument] = process.argv.slice(2);
|
|
136
|
+
if (!root) throw new Error('RELEASES_ROOT is required');
|
|
137
|
+
if (command === 'stage') console.log(stage(root, resolve(argument)));
|
|
138
|
+
else if (command === 'activate') console.log(`Active release ${activate(root, argument).id}`);
|
|
139
|
+
else {
|
|
140
|
+
const host = process.env.HOST || '127.0.0.1', server = releaseServer(root);
|
|
141
|
+
server.listen(Number(process.env.PORT || 0), host, () => console.log(`Release asset server ready on http://${host}:${server.address().port}`));
|
|
142
|
+
}
|
|
143
|
+
}
|