TutorialsRun seed scripts
Script source
Full source code for the TWIN supply-chain seed scripts.
consignmentsCreate
// Copyright 2026 IOTA Stiftung.
// SPDX-License-Identifier: Apache-2.0.
import {
type ISupplyChainAppConsignment,
supplyChainConsignmentPayload
} from "@twin.org/supply-chain-models";
import { getClient } from "./lib/client.js";
export default async function consignmentsCreate(): Promise<void> {
const client = await getClient();
const result = await client.consignmentCreate(createConsignmentPayload());
console.log("Success:", JSON.stringify(result, null, 2));
}
export function createConsignmentPayload(): ISupplyChainAppConsignment {
return {
...supplyChainConsignmentPayload,
identifier: `INT-${crypto.randomUUID().slice(0, 8).toUpperCase()}`
};
}consignmentsCreateWithEvents
// Copyright 2026 IOTA Stiftung.
// SPDX-License-Identifier: Apache-2.0.
import type { IJsonLdNodeObject } from "@twin.org/data-json-ld";
import { UneceContexts, UneceTypes } from "@twin.org/standards-unece";
import {
supplyChainConsignmentPayload,
supplyChainEventTypeCodes
} from "@twin.org/supply-chain-models";
import type { SupplyChainRestClient } from "@twin.org/supply-chain-rest-client";
import { getClient } from "./lib/client.js";
function mkEvent(
typeCode: string,
occurrenceDateTime: string,
extra: Record<string, unknown> = {}
): IJsonLdNodeObject {
return {
"@context": UneceContexts.Context,
type: UneceTypes.SupplyChainEvent,
typeCode,
occurrenceDateTime,
...extra
};
}
const location = (name: string, locationID: string) => ({
type: UneceTypes.LogisticsLocation,
name,
locationID
});
const carrier = (name: string, eori: string, bookingId: string) => ({
carrierAssignedId: bookingId,
carrierParty: {
type: UneceTypes.TradeParty,
name,
registeredId: {
"@type": "https://ref.gs1.org/voc/OrganizationID_Type-EORI",
"@value": eori
}
}
});
const transportMeans = (typeCode = "1") => ({
transportMeans: { type: UneceTypes.TransportMeans, transportMeansTypeCode: typeCode }
});
// ─── Scenario definitions ────────────────────────────────────────────────────
interface Scenario {
label: string;
description: string;
overrides: Partial<typeof supplyChainConsignmentPayload>;
events: IJsonLdNodeObject[];
}
const SCENARIOS: Scenario[] = [
// 1. Planned — only pre-notification received
{
label: "Planned for departure",
description: "Consignment created, pre-notification sent. No despatch yet.",
overrides: {},
events: [mkEvent(supplyChainEventTypeCodes.PreNotification, "2026-05-20T08:00:00.000Z")]
},
// 2. Active — despatched, carrier booked
{
label: "Despatched with carrier",
description: "Goods despatched and carrier booking confirmed.",
overrides: {},
events: [
mkEvent(supplyChainEventTypeCodes.PreNotification, "2026-05-19T08:00:00.000Z"),
mkEvent(supplyChainEventTypeCodes.Despatched, "2026-05-20T09:00:00.000Z"),
mkEvent(supplyChainEventTypeCodes.CarrierBooking, "2026-05-20T11:00:00.000Z", {
...carrier("Maersk Line", "GBMAERSK001", "MSK-20260520-001"),
eventLocation: location("Port of Calais", "FRCDG")
})
]
},
// 3. Active — at location of exit, location operator matched
{
label: "At exit — operator matched",
description: "Goods at exit location. Location operator has confirmed a system match.",
overrides: {},
events: [
mkEvent(supplyChainEventTypeCodes.PreNotification, "2026-05-18T08:00:00.000Z"),
mkEvent(supplyChainEventTypeCodes.Despatched, "2026-05-19T09:00:00.000Z"),
mkEvent(supplyChainEventTypeCodes.CarrierBooking, "2026-05-19T11:00:00.000Z", {
...carrier("CMA CGM", "FRCMA001", "CMA-20260519-042"),
eventLocation: location("Port of Calais", "FRCDG")
}),
mkEvent(supplyChainEventTypeCodes.IssuedDocument, "2026-05-20T10:00:00.000Z"),
mkEvent(supplyChainEventTypeCodes.OnwardSharing, "2026-05-20T14:00:00.000Z"),
mkEvent(supplyChainEventTypeCodes.AtLocationOfExit, "2026-05-22T07:00:00.000Z", {
eventLocation: location("Port of Calais", "FRCDG"),
...transportMeans("1")
}),
mkEvent(supplyChainEventTypeCodes.LocationOperatorMatch, "2026-05-22T09:00:00.000Z", {
eventLocation: location("Felixstowe", "GBFXT"),
operatorName: "Marine Cargo Processing"
})
]
},
// 4. Active — crossed border, at entry
{
label: "In transit — at entry location",
description: "Goods left exit location and arrived at entry location.",
overrides: {},
events: [
mkEvent(supplyChainEventTypeCodes.PreNotification, "2026-05-15T08:00:00.000Z"),
mkEvent(supplyChainEventTypeCodes.Despatched, "2026-05-16T09:00:00.000Z"),
mkEvent(supplyChainEventTypeCodes.CarrierBooking, "2026-05-16T11:00:00.000Z", {
...carrier("P&O Ferries", "GBPOF001", "POF-20260516-007"),
eventLocation: location("Port of Calais", "FRCDG")
}),
mkEvent(supplyChainEventTypeCodes.AtLocationOfExit, "2026-05-18T07:00:00.000Z", {
eventLocation: location("Port of Calais", "FRCDG"),
...transportMeans("1")
}),
mkEvent(supplyChainEventTypeCodes.LocationOperatorMatch, "2026-05-18T09:00:00.000Z", {
eventLocation: location("Port of Dover", "GBDVR"),
operatorName: "Dover Port Authority"
}),
mkEvent(supplyChainEventTypeCodes.LeftLocationOfExit, "2026-05-18T12:00:00.000Z", {
eventLocation: location("Port of Calais", "FRCDG")
}),
mkEvent(supplyChainEventTypeCodes.AtLocationOfEntry, "2026-05-18T14:00:00.000Z", {
eventLocation: location("Port of Dover", "GBDVR"),
...transportMeans("1")
})
]
},
// 5. Delivered — full journey completed
{
label: "Delivered — full journey",
description: "Complete journey from pre-notification through to final destination.",
overrides: {},
events: [
mkEvent(supplyChainEventTypeCodes.PreNotification, "2026-05-10T08:00:00.000Z"),
mkEvent(supplyChainEventTypeCodes.Despatched, "2026-05-11T09:00:00.000Z"),
mkEvent(supplyChainEventTypeCodes.CarrierBooking, "2026-05-11T11:00:00.000Z", {
...carrier("DFDS Seaways", "GBDFD001", "DFDS-20260511-099"),
eventLocation: location("Port of Calais", "FRCDG")
}),
mkEvent(supplyChainEventTypeCodes.IssuedDocument, "2026-05-12T10:00:00.000Z"),
mkEvent(supplyChainEventTypeCodes.OnwardSharing, "2026-05-12T14:00:00.000Z"),
mkEvent(supplyChainEventTypeCodes.AtLocationOfExit, "2026-05-13T07:00:00.000Z", {
eventLocation: location("Port of Calais", "FRCDG"),
...transportMeans("1")
}),
mkEvent(supplyChainEventTypeCodes.LocationOperatorMatch, "2026-05-13T09:00:00.000Z", {
eventLocation: location("Port of Dover", "GBDVR"),
operatorName: "Dover Harbour Board"
}),
mkEvent(supplyChainEventTypeCodes.LeftLocationOfExit, "2026-05-13T12:00:00.000Z", {
eventLocation: location("Port of Calais", "FRCDG")
}),
mkEvent(supplyChainEventTypeCodes.AtLocationOfEntry, "2026-05-13T14:00:00.000Z", {
eventLocation: location("Port of Dover", "GBDVR"),
...transportMeans("1")
}),
mkEvent(supplyChainEventTypeCodes.LeftLocationOfEntry, "2026-05-13T17:00:00.000Z", {
eventLocation: location("Port of Dover", "GBDVR")
}),
mkEvent(supplyChainEventTypeCodes.ArrivedAtFinalDestination, "2026-05-14T10:00:00.000Z", {
eventLocation: location("London Distribution Centre", "GBLDC")
})
]
}
];
// ─── Runner ──────────────────────────────────────────────────────────────────
async function createScenario(client: SupplyChainRestClient, scenario: Scenario): Promise<void> {
const uid = crypto.randomUUID().slice(0, 8).toUpperCase();
const consignment = {
...supplyChainConsignmentPayload,
identifier: `INT-${uid}`,
...scenario.overrides
};
const { id } = await client.consignmentCreate(consignment);
console.log(`\n[${scenario.label}] ${consignment.identifier}`);
console.log(` ${scenario.description}`);
for (const eventPayload of scenario.events) {
const result = await client.consignmentAddEvent(id, eventPayload);
const code = String(eventPayload.typeCode).replace("twin:", "");
console.log(` ✓ ${code} → ${result.id}`);
}
// A despatched goods load is also re-submitted as an update (PUT), which is
// what triggers the ISN despatch signal.
const hasDespatch = scenario.events.some(
eventPayload => eventPayload.typeCode === supplyChainEventTypeCodes.Despatched
);
if (hasDespatch) {
await client.consignmentUpdate(id, {
...consignment,
summaryDescription: `${scenario.label} - goods load updated at despatch`
});
console.log(" ✓ goods load update (PUT)");
}
}
export default async function consignmentsCreateWithEvents(): Promise<void> {
const client = await getClient();
console.log(`\nCreating ${SCENARIOS.length} scenarios...`);
for (const scenario of SCENARIOS) {
await createScenario(client, scenario);
}
console.log("\n✅ All scenarios created.");
}seedDocuments
// Copyright 2026 IOTA Stiftung.
// SPDX-License-Identifier: Apache-2.0.
import { readFileSync } from "node:fs";
import { dirname, isAbsolute, resolve } from "node:path";
import { type IUneceDocument, UneceTypes } from "@twin.org/standards-unece";
import type { SupplyChainRestClient } from "@twin.org/supply-chain-rest-client";
import { getClient } from "./lib/client.js";
interface ISeedDocumentInput extends Omit<IUneceDocument, "attachmentBinaryObject"> {
attachmentBinaryObject?: string;
attachmentPath?: string;
}
export type ISeedDocumentPayload = IUneceDocument & { attachmentBinaryObject: string };
const UNECE_D23B_CONTEXT = "https://vocabulary.uncefact.org/unece-context-D23B.jsonld";
const LEGACY_UNECE_CONTEXT = "https://vocabulary.uncefact.org/unece-context.jsonld";
/**
* Upserts documents for the given consignment.
*/
export default async function seedDocuments(): Promise<void> {
const { consignmentId, documents } = loadConsignmentIdAndDocumentsFromArgs();
const client = await getClient();
try {
await ensureConsignmentExists(client, consignmentId);
await upsertDocumentsForConsignment(client, consignmentId, documents);
} catch (err) {
console.error("Failed to seed documents:", (err as Error).message ?? err);
process.exit(1);
}
}
/**
* Loads consignmentId and documents from process.argv, validates and returns them.
*/
function loadConsignmentIdAndDocumentsFromArgs(): {
consignmentId: string;
documents: ISeedDocumentPayload[];
} {
const [consignmentId, documentsArg] = process.argv.slice(3);
if (!consignmentId || !documentsArg) {
console.error("\n* Missing arguments.");
console.error("> Usage: seedDocuments <CONSIGNMENT_ID> <documents.json | documents-array>");
console.error(
"> Example: seedDocuments aig:... apps/supply-chain-node/scripts/seed/documents.json\n"
);
process.exit(1);
}
try {
return {
consignmentId,
documents: loadDocumentsFromArg(documentsArg)
};
} catch (err) {
console.error("Failed to prepare documents:", (err as Error).message ?? err);
process.exit(1);
}
}
/**
* Load documents from a file path or inline JSON string.
* @param documentsArg The file path or inline JSON.
* @returns The normalized document payloads.
*/
export function loadDocumentsFromArg(documentsArg: string): ISeedDocumentPayload[] {
let documentsData: unknown;
let baseDirectory = process.cwd();
if (documentsArg.endsWith(".json")) {
const filePath = resolve(documentsArg);
const file = readFileSync(filePath, "utf8");
documentsData = JSON.parse(file);
baseDirectory = dirname(filePath);
} else {
documentsData = JSON.parse(documentsArg);
}
return normalizeDocuments(documentsData, baseDirectory);
}
/**
* Ensure the target consignment exists before attempting document upserts.
* @param client The supply chain client.
* @param consignmentId The target consignment id.
*/
export async function ensureConsignmentExists(
client: SupplyChainRestClient,
consignmentId: string
): Promise<void> {
try {
await client.consignmentGet(consignmentId);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (
message.includes("vertexNotFound") ||
message.includes("consignmentNotFound") ||
message.includes("auditableItemGraphService.getFailed") ||
message.includes("supplyChainService.getConsignmentFailed")
) {
throw new Error(`Consignment not found: ${consignmentId}`);
}
throw new Error(`Failed to load consignment ${consignmentId}: ${message}`);
}
}
/**
* Upsert all provided documents for a consignment.
* @param client The supply chain client.
* @param consignmentId The target consignment id.
* @param documents The normalized document payloads.
*/
export async function upsertDocumentsForConsignment(
client: SupplyChainRestClient,
consignmentId: string,
documents: ISeedDocumentPayload[]
): Promise<void> {
for (const [index, document] of documents.entries()) {
const result = await client.documentUpsert(consignmentId, document);
console.log(
`Upserted document ${index + 1}/${documents.length}: ${document.identifier} (${document.versionId}) -> ${result.aigId}`
);
}
console.log(`Created/updated ${documents.length} documents for consignment ${consignmentId}.`);
}
/**
* Normalize a single document object or array to an array of payloads.
* @param documentsData The raw parsed JSON.
* @param baseDirectory The base directory for resolving attachmentPath.
* @returns The normalized document payloads.
*/
function normalizeDocuments(documentsData: unknown, baseDirectory: string): ISeedDocumentPayload[] {
const documents = Array.isArray(documentsData) ? documentsData : [documentsData];
if (documents.length === 0) {
throw new Error("Documents must not be an empty array");
}
return documents.map((document, index) => normalizeDocument(document, index, baseDirectory));
}
/**
* Normalize a document and expand attachmentPath to attachmentBinaryObject.
* @param documentData The raw document input.
* @param index The document index for error messages.
* @param baseDirectory The base directory for resolving attachmentPath.
* @returns The normalized document payload.
*/
function normalizeDocument(
documentData: unknown,
index: number,
baseDirectory: string
): ISeedDocumentPayload {
if (!documentData || typeof documentData !== "object" || Array.isArray(documentData)) {
throw new Error(`Document ${index + 1} must be an object`);
}
const { attachmentPath, attachmentBinaryObject, ...document } =
documentData as ISeedDocumentInput;
const normalizedDocument: ISeedDocumentInput = {
...document,
"@context": normalizeUneceContext(document["@context"]),
type: document.type ?? UneceTypes.Document
};
let encodedAttachment = attachmentBinaryObject;
if (typeof attachmentPath === "string") {
if (!attachmentPath.trim()) {
throw new Error(`Document ${index + 1} has an empty attachmentPath`);
}
const attachmentFilePath = isAbsolute(attachmentPath)
? attachmentPath
: resolve(baseDirectory, attachmentPath);
encodedAttachment = readFileSync(attachmentFilePath).toString("base64");
}
if (typeof encodedAttachment !== "string" || !encodedAttachment.trim()) {
throw new Error(`Document ${index + 1} must include attachmentPath or attachmentBinaryObject`);
}
return {
...normalizedDocument,
attachmentBinaryObject: encodedAttachment
};
}
/**
* Normalize document context to the UNECE D23B context expected by the server schema.
* @param context The raw document @context value.
* @returns The normalized context.
*/
function normalizeUneceContext(context: unknown): IUneceDocument["@context"] {
if (!context) {
return UNECE_D23B_CONTEXT;
}
if (typeof context === "string") {
return (
context === LEGACY_UNECE_CONTEXT ? UNECE_D23B_CONTEXT : context
) as IUneceDocument["@context"];
}
return context as IUneceDocument["@context"];
}seedDocumentsForNewConsignment
// Copyright 2026 IOTA Stiftung.
// SPDX-License-Identifier: Apache-2.0.
import { createConsignmentPayload } from "./consignmentsCreate.js";
import { getClient } from "./lib/client.js";
import { loadDocumentsFromArg, upsertDocumentsForConsignment } from "./seedDocuments.js";
/**
* Creates a new consignment and upserts documents for it.
*/
export default async function seedDocumentsForNewConsignment(): Promise<void> {
const documentsArg = loadDocumentsArgFromArgs();
const client = await getClient();
try {
const consignment = await client.consignmentCreate(createConsignmentPayload());
const documents = loadDocumentsFromArg(documentsArg);
console.log(`Created consignment ${consignment.id}.`);
await upsertDocumentsForConsignment(client, consignment.id, documents);
} catch (err) {
console.error("Failed to seed documents for new consignment:", (err as Error).message ?? err);
process.exit(1);
}
}
/**
* Loads the documents argument from process.argv.
* @returns The file path or inline JSON string.
*/
function loadDocumentsArgFromArgs(): string {
const [documentsArg] = process.argv.slice(3);
if (!documentsArg) {
console.error("\n* Missing arguments.");
console.error("> Usage: seedDocumentsForNewConsignment <documents.json | documents-array>");
console.error(
"> Example: seedDocumentsForNewConsignment apps/supply-chain-node/scripts/seed/documents.json\n"
);
process.exit(1);
}
return documentsArg;
}createManagedLocations
// Copyright 2026 IOTA Stiftung.
// SPDX-License-Identifier: Apache-2.0.
import { readFileSync } from "node:fs";
import { getClient } from "./lib/client.js";
/**
* Creates managed locations for the given org and locations array.
*/
export default async function createManagedLocations(): Promise<void> {
const { orgId, locations } = loadOrgIdAndLocationsFromArgs();
const client = await getClient();
try {
const result = await client.managedLocationUpsert(orgId, locations);
const count = Array.isArray(result) ? result.length : locations.length;
console.log(`Created/updated ${count} managed locations for org ${orgId}.`);
} catch (err) {
console.error("Failed to create managed locations:", (err as Error).message ?? err);
process.exit(1);
}
}
/**
* Loads orgId and locations from process.argv, validates and returns them.
*/
function loadOrgIdAndLocationsFromArgs(): {
orgId: string;
locations: { operator: string; locationName: string; locationId: string; active: boolean }[];
} {
const [orgId, locationsArg] = process.argv.slice(3);
if (!orgId || !locationsArg) {
console.error("\n* Missing arguments.");
console.error("> Usage: createManagedLocations <ORG_ID> <locations.json | locations-array>");
console.error("> Example: createManagedLocations did:... locations.json\n");
process.exit(1);
}
let locations;
try {
if (locationsArg.endsWith(".json")) {
const file = readFileSync(locationsArg, "utf8");
locations = JSON.parse(file);
} else {
locations = JSON.parse(locationsArg);
}
if (!Array.isArray(locations)) throw new Error("Locations must be an array");
} catch (err) {
console.error("Failed to parse locations:", (err as Error).message ?? err);
process.exit(1);
}
return { orgId, locations };
}odrlPoliciesRegister
// Copyright 2026 IOTA Stiftung.
// SPDX-License-Identifier: Apache-2.0.
import type { PolicyAdministrationPointRestClient } from "@twin.org/rights-management-rest-client";
import { getPolicyAdministrationClient, getPublisherOrganizationId } from "./lib/client.js";
const { env } = process;
/**
* The country of the registered ISN the border agency policy authorises,
* matched against the countryId of a SUPPLY_CHAIN_ISN_ENDPOINTS entry.
*/
const ISN_COUNTRY_ID = env.SUPPLY_CHAIN_SEED_ISN_COUNTRY_ID ?? "GB";
/**
* Build a policy in the shape the PAP validator accepts: an ODRL Offer with
* flat refinement constraints (the validator rejects inline "or" groups) and
* a source on each collection. The target carries no refinement, so the
* policy covers every consignment the publisher creates.
* @param assigner The publisher organisation identity.
* @param assigneeRefinement The party refinements selecting the recipient.
* @returns The policy.
*/
function buildPolicy(assigner: string, assigneeRefinement: { [key: string]: unknown }[]): unknown {
return {
"@context": ["http://www.w3.org/ns/odrl.jsonld", "https://schema.twindev.org/odrl/v1/"],
"@type": "Offer",
assigner,
assignee: {
source: "urn:supply-chain:notification-recipients",
refinement: assigneeRefinement
},
target: {
source: "urn:supply-chain:consignments"
},
permission: [{ action: "read" }]
};
}
async function createPolicy(
client: PolicyAdministrationPointRestClient,
label: string,
policy: unknown
): Promise<void> {
try {
const uid = await client.create(
policy as Parameters<PolicyAdministrationPointRestClient["create"]>[0]
);
console.log(`Created the ${label} policy: ${uid}`);
} catch (error) {
console.error(`Creating the ${label} policy failed:`, JSON.stringify(error, undefined, 2));
process.exit(1);
}
}
/**
* Register the ODRL read policies that authorise the ISN (border agency) and
* MCP (location operator) notifications for the seeded consignments. The
* recipient endpoints themselves come from SUPPLY_CHAIN_ISN_ENDPOINTS and
* SUPPLY_CHAIN_MCP_ENDPOINT on the node.
*/
export default async function odrlPoliciesRegister(): Promise<void> {
const assigner = await getPublisherOrganizationId();
console.log(`Registering ODRL read policies for publisher ${assigner}`);
const client = await getPolicyAdministrationClient();
await createPolicy(
client,
"border agency (ISN)",
buildPolicy(assigner, [
{ leftOperand: "information:$.role", operator: "eq", rightOperand: "BorderAgency" },
{
leftOperand: "information:$.countryId",
operator: "eq",
rightOperand: `unece:CountryId#${ISN_COUNTRY_ID}`
}
])
);
await createPolicy(
client,
"location operator (MCP)",
buildPolicy(assigner, [
{ leftOperand: "information:$.role", operator: "eq", rightOperand: "LocationOperator" }
])
);
console.log("Done");
}lib/client
// Copyright 2026 IOTA Stiftung.
// SPDX-License-Identifier: Apache-2.0.
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath, URL } from "node:url";
import { PolicyAdministrationPointRestClient } from "@twin.org/rights-management-rest-client";
import { SupplyChainRestClient } from "@twin.org/supply-chain-rest-client";
import { config } from "dotenv";
config({ path: [".env", ".env.local"] });
const { env } = process;
const HOST = env.SUPPLY_CHAIN_HOST ?? "";
const PORT = env.SUPPLY_CHAIN_PORT ?? "";
const TENANT_API_KEY = env.SUPPLY_CHAIN_TENANT_API_KEY ?? "";
const ADMIN_USER_NAME = env.SUPPLY_CHAIN_ADMIN_USER_NAME ?? "";
const ADMIN_USER_PASSWORD = env.SUPPLY_CHAIN_ADMIN_USER_PASSWORD ?? "";
const ORGANIZATION_IDENTITY = env.SUPPLY_CHAIN_ORGANIZATION_IDENTITY ?? "";
if (!HOST || !PORT || !TENANT_API_KEY || !ADMIN_USER_NAME || !ADMIN_USER_PASSWORD) {
console.error(
"Missing required env vars: SUPPLY_CHAIN_HOST, SUPPLY_CHAIN_PORT, SUPPLY_CHAIN_TENANT_API_KEY, SUPPLY_CHAIN_ADMIN_USER_NAME, SUPPLY_CHAIN_ADMIN_USER_PASSWORD"
);
process.exit(1);
}
const CACHE_PATH = fileURLToPath(new URL("../.token-cache.json", import.meta.url));
const BASE_URL = `http://${HOST}:${PORT}`;
interface TokenCache {
token: string;
expiry: number;
}
function readCache(): TokenCache | null {
try {
const raw = readFileSync(CACHE_PATH, "utf8");
const cache = JSON.parse(raw) as TokenCache;
return cache.expiry > Date.now() + 60_000 ? cache : null;
} catch {
return null;
}
}
function extractAccessToken(setCookie: string | null): string | null {
if (!setCookie) {
return null;
}
const match = setCookie.match(/(?:^|,\s*|;\s*)access_token=([^;]+)/);
return match ? match[1] : null;
}
async function login(): Promise<TokenCache> {
const res = await fetch(`${BASE_URL}/authentication/login`, {
method: "POST",
headers: { "Content-Type": "application/json", "x-api-key": TENANT_API_KEY },
body: JSON.stringify({ email: ADMIN_USER_NAME, password: ADMIN_USER_PASSWORD })
});
if (!res.ok) {
console.error(`Login failed: ${res.status} ${await res.text()}`);
process.exit(1);
}
const data = (await res.json()) as { [key: string]: unknown };
if (typeof data.expiry !== "number") {
console.error(`Unexpected login response shape: ${JSON.stringify(data)}`);
process.exit(1);
}
const token = extractAccessToken(res.headers.get("set-cookie"));
if (!token) {
console.error("Login succeeded but no access_token cookie was returned");
process.exit(1);
}
const cache: TokenCache = { token, expiry: data.expiry };
writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 2));
return cache;
}
async function getSession(): Promise<TokenCache> {
const cached = readCache();
if (cached) {
return cached;
}
console.log("Token expired or missing, logging in...");
return login();
}
export async function getClient(): Promise<SupplyChainRestClient> {
const { token } = await getSession();
const organization =
ORGANIZATION_IDENTITY ||
(JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString()) as { org: string }).org;
return new SupplyChainRestClient({
endpoint: `${BASE_URL}?organization=${encodeURIComponent(organization)}`,
headers: {
Authorization: `Bearer ${token}`,
"x-api-key": TENANT_API_KEY
}
});
}
/**
* Construct an authenticated policy administration point rest client, used to
* register the ODRL policies that authorise notifications.
* @returns The policy administration point rest client.
*/
export async function getPolicyAdministrationClient(): Promise<PolicyAdministrationPointRestClient> {
const { token } = await getSession();
const organization =
ORGANIZATION_IDENTITY ||
(JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString()) as { org: string }).org;
return new PolicyAdministrationPointRestClient({
endpoint: `${BASE_URL}?organization=${encodeURIComponent(organization)}`,
headers: {
Authorization: `Bearer ${token}`,
"x-api-key": TENANT_API_KEY
}
});
}
/**
* Resolve the organisation identity that owns the seeded data, used as the
* assigner when registering ODRL policies. Prefers the configured
* SUPPLY_CHAIN_ORGANIZATION_IDENTITY, otherwise reads the `org` claim from the
* session token.
* @returns The organisation identity (DID).
*/
export async function getPublisherOrganizationId(): Promise<string> {
const organizationIdentity = env.SUPPLY_CHAIN_ORGANIZATION_IDENTITY ?? "";
if (organizationIdentity) {
return organizationIdentity;
}
const { token } = await getSession();
const payloadB64 = token.split(".")[1];
const org =
payloadB64 === undefined
? undefined
: (JSON.parse(Buffer.from(payloadB64, "base64url").toString()) as { org?: string }).org;
if (!org) {
console.error(
"Could not resolve the organisation identity from the session token; set SUPPLY_CHAIN_ORGANIZATION_IDENTITY."
);
process.exit(1);
}
return org;
}