@hotmeshio/long-tail 0.28.0 → 0.28.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/build/api/files.d.ts +5 -0
- package/build/api/files.js +19 -2
- package/build/api/knowledge.d.ts +3 -1
- package/build/lib/db/schemas/042_knowledge_edition_labels.sql +9 -0
- package/build/lib/machines/line.d.ts +3 -0
- package/build/lib/machines/line.js +7 -1
- package/build/lib/machines/protocol/codec.d.ts +6 -0
- package/build/lib/machines/protocol/codec.js +1 -1
- package/build/lib/machines/protocol/contract.d.ts +2 -0
- package/build/lib/machines/protocol/contract.js +12 -7
- package/build/lib/machines/site/mosquitto-conf.d.ts +7 -3
- package/build/lib/machines/site/mosquitto-conf.js +3 -8
- package/build/lib/machines/site/site-conf.d.ts +4 -1
- package/build/lib/machines/site/site-conf.js +5 -1
- package/build/lib/machines/transport/nats-bus.d.ts +2 -0
- package/build/lib/machines/transport/stream-bus.d.ts +5 -1
- package/build/lib/machines/transport/stream-bus.js +12 -4
- package/build/lib/machines/transport/streams.d.ts +10 -2
- package/build/lib/machines/transport/streams.js +32 -5
- package/build/lib/messaging/memory.js +3 -0
- package/build/lib/messaging/nats/streams.d.ts +3 -0
- package/build/lib/messaging/nats/streams.js +16 -0
- package/build/lib/storage/local.d.ts +2 -0
- package/build/lib/storage/local.js +3 -0
- package/build/lib/storage/s3.d.ts +3 -2
- package/build/lib/storage/s3.js +6 -1
- package/build/lib/storage/types.d.ts +7 -1
- package/build/routes/file-browser.js +3 -1
- package/build/routes/knowledge.js +6 -5
- package/build/routes/machines.js +33 -0
- package/build/services/knowledge/index.d.ts +4 -2
- package/build/services/knowledge/index.js +8 -2
- package/build/services/knowledge/lookup-cache.d.ts +8 -0
- package/build/services/knowledge/lookup-cache.js +30 -3
- package/build/services/knowledge/lookup-refs.js +1 -1
- package/build/services/knowledge/sql.d.ts +4 -2
- package/build/services/knowledge/sql.js +8 -3
- package/build/services/machines/config.d.ts +2 -0
- package/build/services/machines/config.js +1 -0
- package/build/services/machines/contracts.d.ts +18 -0
- package/build/services/machines/contracts.js +15 -1
- package/build/services/machines/forms.d.ts +26 -1
- package/build/services/machines/forms.js +17 -1
- package/build/services/machines/gateway/actions.d.ts +13 -1
- package/build/services/machines/gateway/admission.d.ts +15 -1
- package/build/services/machines/gateway/admission.js +47 -0
- package/build/services/machines/gateway/api-escalations.js +4 -0
- package/build/services/machines/gateway/dispatcher.d.ts +9 -4
- package/build/services/machines/gateway/dispatcher.js +42 -14
- package/build/services/machines/gateway/executor.d.ts +2 -0
- package/build/services/machines/gateway/executor.js +18 -2
- package/build/services/machines/gateway/gateway.d.ts +5 -0
- package/build/services/machines/gateway/gateway.js +11 -3
- package/build/services/machines/gateway/log.d.ts +2 -0
- package/build/services/machines/gateway/log.js +2 -0
- package/build/services/machines/gateway/lt-http.js +11 -2
- package/build/services/machines/gateway/pacing.js +1 -1
- package/build/services/machines/gateway/plan-machine.js +26 -11
- package/build/services/machines/gateway/plan-shared.d.ts +5 -3
- package/build/services/machines/gateway/plan-shared.js +6 -4
- package/build/services/machines/gateway/reply-waiter.d.ts +2 -0
- package/build/services/machines/gateway/state.d.ts +24 -1
- package/build/services/machines/gateway/state.js +67 -6
- package/build/services/machines/gateway/wire-window.d.ts +21 -0
- package/build/services/machines/gateway/wire-window.js +61 -0
- package/build/services/machines/gateway/wire.js +1 -0
- package/build/services/machines/roles.d.ts +5 -4
- package/build/services/machines/roles.js +13 -4
- package/build/services/machines/select.d.ts +3 -2
- package/build/services/machines/select.js +4 -3
- package/build/services/machines/surface.d.ts +10 -1
- package/build/services/machines/surface.js +20 -4
- package/build/system/activities/knowledge.d.ts +4 -0
- package/build/system/activities/knowledge.js +15 -0
- package/build/system/activities/sql.d.ts +2 -0
- package/build/system/activities/sql.js +6 -1
- package/build/system/mcp-servers/knowledge.js +4 -2
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/types/escalation.d.ts +9 -6
- package/build/types/escalation.js +8 -2
- package/build/types/machines-view.d.ts +16 -0
- package/build/types/machines.d.ts +1 -0
- package/build/types/messaging.d.ts +2 -0
- package/build/types/startup.d.ts +2 -0
- package/docs/dashboard.md +1 -1
- package/docs/hitl/lookups.md +6 -4
- package/docs/machines.md +25 -16
- package/package.json +1 -1
package/build/api/files.d.ts
CHANGED
|
@@ -10,9 +10,14 @@ export declare function getFileMetadata(input: {
|
|
|
10
10
|
export declare function deleteFile(input: {
|
|
11
11
|
filePath: string;
|
|
12
12
|
}): Promise<LTApiResult>;
|
|
13
|
+
/**
|
|
14
|
+
* Stores the bytes with their SHA-256. A caller that names the digest it
|
|
15
|
+
* expects gets 422 when the bytes received differ, and nothing is stored.
|
|
16
|
+
*/
|
|
13
17
|
export declare function uploadFile(input: {
|
|
14
18
|
path: string;
|
|
15
19
|
buffer: Buffer;
|
|
20
|
+
sha256?: string;
|
|
16
21
|
}): Promise<LTApiResult>;
|
|
17
22
|
export declare function generateSignedUrl(input: {
|
|
18
23
|
filePath: string;
|
package/build/api/files.js
CHANGED
|
@@ -5,6 +5,7 @@ exports.getFileMetadata = getFileMetadata;
|
|
|
5
5
|
exports.deleteFile = deleteFile;
|
|
6
6
|
exports.uploadFile = uploadFile;
|
|
7
7
|
exports.generateSignedUrl = generateSignedUrl;
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
8
9
|
const storage_1 = require("../lib/storage");
|
|
9
10
|
const mime_1 = require("../lib/storage/mime");
|
|
10
11
|
const publish_1 = require("../lib/events/publish");
|
|
@@ -46,9 +47,25 @@ async function deleteFile(input) {
|
|
|
46
47
|
return { status: 500, error: err.message };
|
|
47
48
|
}
|
|
48
49
|
}
|
|
50
|
+
const SHA256_HEX = /^[0-9a-f]{64}$/i;
|
|
51
|
+
/**
|
|
52
|
+
* Stores the bytes with their SHA-256. A caller that names the digest it
|
|
53
|
+
* expects gets 422 when the bytes received differ, and nothing is stored.
|
|
54
|
+
*/
|
|
49
55
|
async function uploadFile(input) {
|
|
56
|
+
if (input.sha256 !== undefined && !SHA256_HEX.test(input.sha256)) {
|
|
57
|
+
return { status: 400, error: 'sha256 must be 64 hex characters' };
|
|
58
|
+
}
|
|
59
|
+
const sha256 = (0, crypto_1.createHash)('sha256').update(input.buffer).digest('hex');
|
|
60
|
+
if (input.sha256 && input.sha256.toLowerCase() !== sha256) {
|
|
61
|
+
return {
|
|
62
|
+
status: 422,
|
|
63
|
+
error: 'sha256 does not match the bytes received',
|
|
64
|
+
data: { error: 'sha256 does not match the bytes received', path: input.path, expected: input.sha256.toLowerCase(), actual: sha256 },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
50
67
|
try {
|
|
51
|
-
const result = await (0, storage_1.getStorageBackend)().write(input.path, input.buffer);
|
|
68
|
+
const result = await (0, storage_1.getStorageBackend)().write(input.path, input.buffer, { sha256 });
|
|
52
69
|
const contentType = (0, mime_1.mimeFromPath)(input.path);
|
|
53
70
|
(0, publish_1.publishFileEvent)({
|
|
54
71
|
type: 'file.stored',
|
|
@@ -58,7 +75,7 @@ async function uploadFile(input) {
|
|
|
58
75
|
});
|
|
59
76
|
return {
|
|
60
77
|
status: 200,
|
|
61
|
-
data: { path: input.path, size: result.size, content_type: contentType },
|
|
78
|
+
data: { path: input.path, size: result.size, content_type: contentType, sha256 },
|
|
62
79
|
};
|
|
63
80
|
}
|
|
64
81
|
catch (err) {
|
package/build/api/knowledge.d.ts
CHANGED
|
@@ -14,7 +14,8 @@ export declare function getEntry(input: {
|
|
|
14
14
|
export declare function getEntryVersion(input: {
|
|
15
15
|
domain: string;
|
|
16
16
|
key: string;
|
|
17
|
-
|
|
17
|
+
/** An edition number, or the label an edition carries. */
|
|
18
|
+
version: number | string;
|
|
18
19
|
}): Promise<LTApiResult>;
|
|
19
20
|
export declare function listEntryVersions(input: {
|
|
20
21
|
domain: string;
|
|
@@ -26,6 +27,7 @@ export declare function storeEntry(input: {
|
|
|
26
27
|
data: Record<string, any>;
|
|
27
28
|
tags?: string[];
|
|
28
29
|
replace?: boolean;
|
|
30
|
+
label?: string;
|
|
29
31
|
}): Promise<LTApiResult>;
|
|
30
32
|
export declare function setField(input: {
|
|
31
33
|
domain: string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- An edition may carry a label: a name a lookup ref pins instead of the
|
|
2
|
+
-- version number (a catalog revision, say). A label names one edition of an
|
|
3
|
+
-- entry; editions without one are reached by number or as current.
|
|
4
|
+
ALTER TABLE lt_knowledge_versions
|
|
5
|
+
ADD COLUMN IF NOT EXISTS label TEXT;
|
|
6
|
+
|
|
7
|
+
CREATE UNIQUE INDEX IF NOT EXISTS lt_knowledge_versions_label_idx
|
|
8
|
+
ON lt_knowledge_versions (domain, key, label)
|
|
9
|
+
WHERE label IS NOT NULL;
|
|
@@ -35,6 +35,7 @@ export declare function controllerId(line: LineDef): string;
|
|
|
35
35
|
export declare function stationRole(process: string): string;
|
|
36
36
|
export declare const DEFAULT_EXIT_ROLE = "line-exit";
|
|
37
37
|
export declare const DEFAULT_OPERATOR_ROLE = "line-operator";
|
|
38
|
+
export declare const DEFAULT_QUALITY_ROLE = "line-quality";
|
|
38
39
|
/** The Long Tail role a machine resolves handoffs under. */
|
|
39
40
|
export declare function machineRole(def: Pick<MachineDef, 'role' | 'process'>): string;
|
|
40
41
|
/** Every station role on the line, once each, in route order. */
|
|
@@ -45,3 +46,5 @@ export declare function exitRole(line: LineDef): string;
|
|
|
45
46
|
export declare function receiverRoles(line: LineDef): string[];
|
|
46
47
|
/** The role of the person who decides failed handoffs and closes alarm and machine rows. */
|
|
47
48
|
export declare function operatorRole(line: LineDef): string;
|
|
49
|
+
/** The role of the people who decide units a machine finished on hold: release, rework or scrap. */
|
|
50
|
+
export declare function qualityRole(line: LineDef): string;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* several separated by commas.
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.DEFAULT_OPERATOR_ROLE = exports.DEFAULT_EXIT_ROLE = exports.CARRIER_ROLE = exports.ADDRESSING = void 0;
|
|
8
|
+
exports.DEFAULT_QUALITY_ROLE = exports.DEFAULT_OPERATOR_ROLE = exports.DEFAULT_EXIT_ROLE = exports.CARRIER_ROLE = exports.ADDRESSING = void 0;
|
|
9
9
|
exports.learnsAddress = learnsAddress;
|
|
10
10
|
exports.validateLine = validateLine;
|
|
11
11
|
exports.loadLine = loadLine;
|
|
@@ -20,6 +20,7 @@ exports.stationRoles = stationRoles;
|
|
|
20
20
|
exports.exitRole = exitRole;
|
|
21
21
|
exports.receiverRoles = receiverRoles;
|
|
22
22
|
exports.operatorRole = operatorRole;
|
|
23
|
+
exports.qualityRole = qualityRole;
|
|
23
24
|
const fs_1 = require("fs");
|
|
24
25
|
const protocol_1 = require("./protocol");
|
|
25
26
|
/** How a line knows its machines' addresses (`LineDef.addressing`). */
|
|
@@ -109,6 +110,7 @@ function stationRole(process) {
|
|
|
109
110
|
}
|
|
110
111
|
exports.DEFAULT_EXIT_ROLE = 'line-exit';
|
|
111
112
|
exports.DEFAULT_OPERATOR_ROLE = 'line-operator';
|
|
113
|
+
exports.DEFAULT_QUALITY_ROLE = 'line-quality';
|
|
112
114
|
/** The Long Tail role a machine resolves handoffs under. */
|
|
113
115
|
function machineRole(def) {
|
|
114
116
|
return def.role ?? stationRole(def.process);
|
|
@@ -129,3 +131,7 @@ function receiverRoles(line) {
|
|
|
129
131
|
function operatorRole(line) {
|
|
130
132
|
return line.roles?.operator ?? exports.DEFAULT_OPERATOR_ROLE;
|
|
131
133
|
}
|
|
134
|
+
/** The role of the people who decide units a machine finished on hold: release, rework or scrap. */
|
|
135
|
+
function qualityRole(line) {
|
|
136
|
+
return line.roles?.quality ?? exports.DEFAULT_QUALITY_ROLE;
|
|
137
|
+
}
|
|
@@ -20,6 +20,12 @@ export interface MachineCommand {
|
|
|
20
20
|
attempt?: number;
|
|
21
21
|
/** The command may have reached the machine before: sent earlier, or its row predates this gateway's takeover. */
|
|
22
22
|
retry?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* When the command was issued, ISO 8601: the same on every send of this `cmdId`, by this gateway or the
|
|
25
|
+
* next, so a protocol that stamps a time repeats the first attempt. A dispatch or a command row takes the
|
|
26
|
+
* start of its validity round; any other command the time of its first send.
|
|
27
|
+
*/
|
|
28
|
+
issuedAt?: string;
|
|
23
29
|
}
|
|
24
30
|
/** MQTT filters under each machine's topic, e.g. `job` or `alarm/+`. */
|
|
25
31
|
export interface CodecChannels {
|
|
@@ -83,7 +83,7 @@ function genericCodec(opts) {
|
|
|
83
83
|
return receiptFrom === exports.RECEIPT_FROM.ARRIVED ? [seen] : inferReceipt(seen, receiptFrom);
|
|
84
84
|
},
|
|
85
85
|
encode(ident, command) {
|
|
86
|
-
return [{ topic: (0, topics_1.machineTopic)(opts.root, ident, constants_1.CHANNEL.CMD), payload: JSON.stringify((0, envelope_1.buildEnvelope)(ident, command)) }];
|
|
86
|
+
return [{ topic: (0, topics_1.machineTopic)(opts.root, ident, constants_1.CHANNEL.CMD), payload: JSON.stringify((0, envelope_1.buildEnvelope)(ident, command, { at: command.issuedAt })) }];
|
|
87
87
|
},
|
|
88
88
|
};
|
|
89
89
|
}
|
|
@@ -17,3 +17,5 @@ export interface Inspection {
|
|
|
17
17
|
}
|
|
18
18
|
/** Validate one message. `retained` is the publisher's flag when the transport reports it. */
|
|
19
19
|
export declare function inspectMessage(root: string, topic: string, raw: string | Buffer, retained?: boolean): Inspection;
|
|
20
|
+
/** A receiver's report that it could read no id at all: a transfer failure on the read, naming no unit and no carrier. */
|
|
21
|
+
export declare function isUnreadableRead(b: Record<string, any>): boolean;
|
|
@@ -10,6 +10,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.inspectMessage = inspectMessage;
|
|
13
|
+
exports.isUnreadableRead = isUnreadableRead;
|
|
13
14
|
const ajv_1 = __importDefault(require("ajv"));
|
|
14
15
|
const constants_1 = require("./constants");
|
|
15
16
|
const envelope_1 = require("./envelope");
|
|
@@ -41,7 +42,7 @@ const BODY = {
|
|
|
41
42
|
jobs: { type: 'array', items: shape({ jobId: STR, units: { type: 'array', items: STR }, status: STR }) },
|
|
42
43
|
alarms: { type: 'array', items: STR },
|
|
43
44
|
cycles: INT0,
|
|
44
|
-
}),
|
|
45
|
+
}, ['bootId', 'software', 'recipe', 'cycles']),
|
|
45
46
|
[topics_1.MESSAGE_KIND.JOB]: shape({
|
|
46
47
|
event: oneOf(constants_1.JOB_EVENT), jobId: STR, unitId: STR, orderRef: STR, recipe: RECIPE, carrierId: STR_N, peer: STR_N,
|
|
47
48
|
result: oneOf(constants_1.JOB_RESULT), reason: STR_N,
|
|
@@ -52,15 +53,15 @@ const BODY = {
|
|
|
52
53
|
materials: { type: 'array', items: shape({ material: STR, lot: STR }) },
|
|
53
54
|
startedAt: TS, endedAt: TS, durationMs: INT0, failure: oneOf(constants_1.TRANSFER_FAILURE),
|
|
54
55
|
percent: { type: 'number', minimum: 0, maximum: 100 }, remainingSec: INT0, stage: STR, detail: { type: 'object' },
|
|
55
|
-
}, ['jobId', 'unitId', 'peer', 'result', 'reason', 'measurements', 'materials', 'startedAt', 'endedAt', 'durationMs', 'failure', 'percent', 'remainingSec', 'stage', 'detail']),
|
|
56
|
+
}, ['jobId', 'unitId', 'orderRef', 'peer', 'result', 'reason', 'measurements', 'materials', 'startedAt', 'endedAt', 'durationMs', 'failure', 'percent', 'remainingSec', 'stage', 'detail']),
|
|
56
57
|
[topics_1.MESSAGE_KIND.ALARM]: shape({
|
|
57
58
|
alarmId: STR, code: STR, level: oneOf(constants_1.LEVEL), text: STR, active: BOOL,
|
|
58
59
|
raisedAt: TS, clearedAt: TS_N, clearedBy: STR_N, ackedBy: STR_N, ackedAt: TS_N,
|
|
59
|
-
zone: STR_N, autoClears: BOOL, action: STR_N, jobId: STR_N,
|
|
60
|
-
}),
|
|
60
|
+
zone: STR_N, autoClears: BOOL, action: STR_N, jobId: STR_N, needsPerson: BOOL,
|
|
61
|
+
}, ['needsPerson']),
|
|
61
62
|
[topics_1.MESSAGE_KIND.TELEMETRY]: shape({ readings: { type: 'object', additionalProperties: NUM } }),
|
|
62
63
|
[topics_1.MESSAGE_KIND.CMD]: shape({ cmdId: STR, op: oneOf(constants_1.OP), args: { type: 'object' } }),
|
|
63
|
-
[topics_1.MESSAGE_KIND.REPLY]: shape({ cmdId: STR, status: oneOf(constants_1.REPLY_STATUS), reason: STR, code: STR }, ['code']),
|
|
64
|
+
[topics_1.MESSAGE_KIND.REPLY]: shape({ cmdId: STR, status: oneOf(constants_1.REPLY_STATUS), reason: STR, code: STR, preparing: BOOL }, ['code', 'preparing']),
|
|
64
65
|
[topics_1.MESSAGE_KIND.REPORT]: { type: 'object' },
|
|
65
66
|
};
|
|
66
67
|
const ajv = new ajv_1.default({ allErrors: true, strict: false });
|
|
@@ -95,6 +96,10 @@ function inspectMessage(root, topic, raw, retained) {
|
|
|
95
96
|
}
|
|
96
97
|
/** Job events a receiver may report naming only the carrier it scanned. */
|
|
97
98
|
const CARRIER_EVENTS = [constants_1.JOB_EVENT.ARRIVED, constants_1.JOB_EVENT.TRANSFER_FAILED];
|
|
99
|
+
/** A receiver's report that it could read no id at all: a transfer failure on the read, naming no unit and no carrier. */
|
|
100
|
+
function isUnreadableRead(b) {
|
|
101
|
+
return b.event === constants_1.JOB_EVENT.TRANSFER_FAILED && b.failure === constants_1.TRANSFER_FAILURE.UNREADABLE_ID && b.unitId === undefined && typeof b.carrierId !== 'string';
|
|
102
|
+
}
|
|
98
103
|
function crossFieldRules(parsed, env, retained) {
|
|
99
104
|
const out = [];
|
|
100
105
|
const b = env.body ?? {};
|
|
@@ -105,8 +110,8 @@ function crossFieldRules(parsed, env, retained) {
|
|
|
105
110
|
if (kind === topics_1.MESSAGE_KIND.ALARM && b.alarmId !== key)
|
|
106
111
|
out.push('alarmId disagrees with the topic');
|
|
107
112
|
if (kind === topics_1.MESSAGE_KIND.JOB) {
|
|
108
|
-
// A scan at a receiver may read only the carrier; every other job event names its unit and job.
|
|
109
|
-
const byCarrier = CARRIER_EVENTS.includes(b.event) && typeof b.carrierId === 'string';
|
|
113
|
+
// A scan at a receiver may read only the carrier, or nothing at all when the read failed; every other job event names its unit and job.
|
|
114
|
+
const byCarrier = CARRIER_EVENTS.includes(b.event) && (typeof b.carrierId === 'string' || isUnreadableRead(b));
|
|
110
115
|
if (b.unitId === undefined && !byCarrier)
|
|
111
116
|
out.push(`${b.event} without a unitId`);
|
|
112
117
|
if (b.jobId === undefined && !byCarrier)
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* NATS, where JetStream keeps them.
|
|
8
8
|
*/
|
|
9
9
|
import { type LineCodec } from '../protocol/codec';
|
|
10
|
-
import { type
|
|
10
|
+
import { type LineDef } from '../line';
|
|
11
|
+
import { type Lines, type TopicRoot } from './site-conf';
|
|
11
12
|
export interface MosquittoConfOptions {
|
|
12
|
-
root:
|
|
13
|
+
root: TopicRoot;
|
|
13
14
|
/** Certificate directory as the broker sees it. */
|
|
14
15
|
certDir: string;
|
|
15
16
|
/** Where the site NATS MQTT listener is, host:port. */
|
|
@@ -25,4 +26,7 @@ export declare function renderMosquittoConf(lines: Lines, opts: MosquittoConfOpt
|
|
|
25
26
|
* the channels under its own name (%u), so its certificate's common name must be its address. On a line
|
|
26
27
|
* that also declares addresses, a certificate named for a declared address is denied that station's channels.
|
|
27
28
|
*/
|
|
28
|
-
export
|
|
29
|
+
export type AclCodec = Pick<LineCodec, 'root' | 'channels' | 'topicFor'>;
|
|
30
|
+
/** One codec for every line, or the codec of each line. */
|
|
31
|
+
export type AclCodecs = AclCodec | ((line: LineDef) => AclCodec);
|
|
32
|
+
export declare function renderMosquittoAcl(lines: Lines, codec: AclCodecs): string;
|
|
@@ -26,7 +26,7 @@ remote_clientid ${(0, site_conf_1.bridgeId)(line)}
|
|
|
26
26
|
cleansession false
|
|
27
27
|
try_private false
|
|
28
28
|
notifications false
|
|
29
|
-
topic ${opts.root}/${line.siteId}/${line.lineId}/# out 1
|
|
29
|
+
topic ${(0, site_conf_1.rootOf)(opts.root, line)}/${line.siteId}/${line.lineId}/# out 1
|
|
30
30
|
`;
|
|
31
31
|
}
|
|
32
32
|
/** The broker for one line or several: one listener, one bridge per line. */
|
|
@@ -53,17 +53,12 @@ max_packet_size ${opts.maxPacketBytes ?? 262_144}
|
|
|
53
53
|
|
|
54
54
|
${list.map((line) => bridgeBlock(line, opts)).join('\n')}`;
|
|
55
55
|
}
|
|
56
|
-
/**
|
|
57
|
-
* The codec's channels decide the topics: a machine writes its events and telemetry and reads its commands.
|
|
58
|
-
* A station whose address the line learns has no user of its own: `pattern` rules give every certificate
|
|
59
|
-
* the channels under its own name (%u), so its certificate's common name must be its address. On a line
|
|
60
|
-
* that also declares addresses, a certificate named for a declared address is denied that station's channels.
|
|
61
|
-
*/
|
|
62
56
|
function renderMosquittoAcl(lines, codec) {
|
|
63
57
|
const list = (0, site_conf_1.linesOf)(lines);
|
|
64
58
|
if (!list.length)
|
|
65
59
|
throw new Error('renderMosquittoAcl needs at least one line');
|
|
66
|
-
|
|
60
|
+
const codecOf = (line) => (typeof codec === 'function' ? codec(line) : codec);
|
|
61
|
+
return `# Rendered from the line definition by site/setup.ts. Do not edit by hand.\n\n${list.map((line) => aclRules(line, codecOf(line))).join('\n\n')}\n`;
|
|
67
62
|
}
|
|
68
63
|
/** One line's rules: its stations, its controller, and the shadows a learning line needs. */
|
|
69
64
|
function aclRules(line, codec) {
|
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
* site NATS stands alone; Long Tail's escalation events stay inside Long Tail.
|
|
9
9
|
*/
|
|
10
10
|
import { type LineDef } from '../line';
|
|
11
|
+
/** One topic root for every line, or a root per line (lines on different codecs). */
|
|
12
|
+
export type TopicRoot = string | ((line: LineDef) => string);
|
|
13
|
+
export declare function rootOf(root: TopicRoot, line: LineDef): string;
|
|
11
14
|
export interface SiteConfOptions {
|
|
12
|
-
root:
|
|
15
|
+
root: TopicRoot;
|
|
13
16
|
gatewayUser: string;
|
|
14
17
|
gatewayPassword: string;
|
|
15
18
|
certDir: string;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* site NATS stands alone; Long Tail's escalation events stay inside Long Tail.
|
|
10
10
|
*/
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.rootOf = rootOf;
|
|
12
13
|
exports.bridgeId = bridgeId;
|
|
13
14
|
exports.gatewayPublish = gatewayPublish;
|
|
14
15
|
exports.linesOf = linesOf;
|
|
@@ -17,6 +18,9 @@ const line_1 = require("../line");
|
|
|
17
18
|
const protocol_1 = require("../protocol");
|
|
18
19
|
const permissions_1 = require("../../messaging/nats/permissions");
|
|
19
20
|
const streams_1 = require("../transport/streams");
|
|
21
|
+
function rootOf(root, line) {
|
|
22
|
+
return typeof root === 'string' ? root : root(line);
|
|
23
|
+
}
|
|
20
24
|
const q = JSON.stringify;
|
|
21
25
|
/** The machine broker's bridge identity for a line. */
|
|
22
26
|
function bridgeId(line) {
|
|
@@ -44,7 +48,7 @@ function renderSiteConf(lines, opts) {
|
|
|
44
48
|
const list = linesOf(lines);
|
|
45
49
|
if (!list.length)
|
|
46
50
|
throw new Error('renderSiteConf needs at least one line');
|
|
47
|
-
const baseOf = (line) => (0, protocol_1.toSubject)(`${opts.root}/${line.siteId}/${line.lineId}`);
|
|
51
|
+
const baseOf = (line) => (0, protocol_1.toSubject)(`${rootOf(opts.root, line)}/${line.siteId}/${line.lineId}`);
|
|
48
52
|
const bridge = list.map((line) => [
|
|
49
53
|
` { user: ${q(`CN=${bridgeId(line)}`)}, allowed_connection_types: ["MQTT"],`,
|
|
50
54
|
` permissions: { publish: [${q(`${baseOf(line)}.>`)}], subscribe: { deny: [">"] } } }`,
|
|
@@ -17,6 +17,8 @@ export interface NatsBusOptions {
|
|
|
17
17
|
lineId: string;
|
|
18
18
|
/** The codec's topics: which subjects are durable events and which are live telemetry. */
|
|
19
19
|
codec: StreamCodec;
|
|
20
|
+
/** The codecs of every line on this site, so the streams keep all their subjects. */
|
|
21
|
+
siteCodecs?: StreamCodec[];
|
|
20
22
|
nakDelayMs?: number;
|
|
21
23
|
}
|
|
22
24
|
export declare class NatsMachineBus implements MachineBus {
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* line's durable consumer on LINE_EVENTS (replayed after a restart); telemetry
|
|
4
4
|
* is read live, since it only proves a machine is alive. Commands leave through
|
|
5
5
|
* an MQTT session at QoS 1 on the site broker, which queues them for a machine
|
|
6
|
-
* that is away; retained machine state is read there too.
|
|
6
|
+
* that is away; retained machine state is read there too. The streams are
|
|
7
|
+
* ensured with every codec on the site, so lines on different roots share
|
|
8
|
+
* them, and a consumer the stream would feed nothing refuses to start.
|
|
7
9
|
*/
|
|
8
10
|
import type { LTPubSub, LTStreams } from '../../../types/messaging';
|
|
9
11
|
import type { MqttEndpoint } from '../../../services/machines/config';
|
|
@@ -19,6 +21,8 @@ export interface StreamBusOptions {
|
|
|
19
21
|
lineId: string;
|
|
20
22
|
/** The codec's topics: which subjects are durable events and which are live telemetry. */
|
|
21
23
|
codec: StreamCodec;
|
|
24
|
+
/** The codecs of every line on this site; the streams keep all their subjects, whichever line starts last. */
|
|
25
|
+
siteCodecs?: StreamCodec[];
|
|
22
26
|
nakDelayMs?: number;
|
|
23
27
|
}
|
|
24
28
|
export declare class StreamMachineBus implements MachineBus {
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* line's durable consumer on LINE_EVENTS (replayed after a restart); telemetry
|
|
5
5
|
* is read live, since it only proves a machine is alive. Commands leave through
|
|
6
6
|
* an MQTT session at QoS 1 on the site broker, which queues them for a machine
|
|
7
|
-
* that is away; retained machine state is read there too.
|
|
7
|
+
* that is away; retained machine state is read there too. The streams are
|
|
8
|
+
* ensured with every codec on the site, so lines on different roots share
|
|
9
|
+
* them, and a consumer the stream would feed nothing refuses to start.
|
|
8
10
|
*/
|
|
9
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
12
|
exports.StreamMachineBus = void 0;
|
|
@@ -23,10 +25,16 @@ class StreamMachineBus {
|
|
|
23
25
|
return `${this.opts.controllerId}-events`;
|
|
24
26
|
}
|
|
25
27
|
async connect() {
|
|
26
|
-
const { streams, codec, siteId, lineId, controllerId } = this.opts;
|
|
27
|
-
for (const spec of (0, streams_1.lineStreamSpecs)(codec))
|
|
28
|
+
const { streams, codec, siteId, lineId, controllerId, siteCodecs = [] } = this.opts;
|
|
29
|
+
for (const spec of (0, streams_1.lineStreamSpecs)([codec, ...siteCodecs]))
|
|
28
30
|
await streams.ensureStream(spec);
|
|
29
|
-
|
|
31
|
+
const consumer = (0, streams_1.lineEventConsumerSpec)(codec, siteId, lineId, this.durableName);
|
|
32
|
+
await streams.ensureConsumer(streams_1.LINE_STREAM.EVENTS, consumer);
|
|
33
|
+
const events = await streams.describeStream(streams_1.LINE_STREAM.EVENTS);
|
|
34
|
+
const missing = events ? (0, streams_1.uncoveredSubjects)(events, consumer.filterSubjects) : consumer.filterSubjects;
|
|
35
|
+
if (missing.length) {
|
|
36
|
+
throw new Error(`${streams_1.LINE_STREAM.EVENTS} keeps nothing of ${missing.join(', ')}; the stream holds ${events?.subjects.join(', ') ?? 'nothing'}`);
|
|
37
|
+
}
|
|
30
38
|
this.session = await mqtt_session_1.MqttSession.open(this.opts.mqtt, controllerId);
|
|
31
39
|
}
|
|
32
40
|
async consume(handler) {
|
|
@@ -13,8 +13,16 @@ export declare const LINE_STREAM: {
|
|
|
13
13
|
export type StreamCodec = Pick<LineCodec, 'root' | 'channels' | 'topicFor'>;
|
|
14
14
|
/** An MQTT filter as a subject: levels become tokens, `+` becomes `*`, `#` becomes `>`. */
|
|
15
15
|
export declare function filterToSubject(filter: string): string;
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The site's streams: every line under each codec's root, since lines on
|
|
18
|
+
* different protocols share one site NATS. A root alone means the generic
|
|
19
|
+
* layout; several codecs give the union of their subjects, in order.
|
|
20
|
+
*/
|
|
21
|
+
export declare function lineStreamSpecs(codecs: StreamCodec | string | (StreamCodec | string)[]): StreamSpec[];
|
|
22
|
+
/** Whether a stream subject pattern keeps everything a consumer filter would read. */
|
|
23
|
+
export declare function subjectCovers(stream: string, filter: string): boolean;
|
|
24
|
+
/** The consumer's filter subjects the stream keeps nothing of. */
|
|
25
|
+
export declare function uncoveredSubjects(stream: Pick<StreamSpec, 'subjects'>, filters: string[]): string[];
|
|
18
26
|
/** The telemetry subjects for one line, read live rather than replayed. */
|
|
19
27
|
export declare function telemetrySubjects(codec: StreamCodec | string, siteId: string, lineId: string): string[];
|
|
20
28
|
/**
|
|
@@ -8,6 +8,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
8
8
|
exports.LINE_STREAM = void 0;
|
|
9
9
|
exports.filterToSubject = filterToSubject;
|
|
10
10
|
exports.lineStreamSpecs = lineStreamSpecs;
|
|
11
|
+
exports.subjectCovers = subjectCovers;
|
|
12
|
+
exports.uncoveredSubjects = uncoveredSubjects;
|
|
11
13
|
exports.telemetrySubjects = telemetrySubjects;
|
|
12
14
|
exports.lineEventConsumerSpec = lineEventConsumerSpec;
|
|
13
15
|
const codec_1 = require("../protocol/codec");
|
|
@@ -26,14 +28,39 @@ function filterToSubject(filter) {
|
|
|
26
28
|
function subjectsFor(codec, channels, siteId, lineId) {
|
|
27
29
|
return channels.map((c) => filterToSubject((0, codec_1.topicOf)(codec, { siteId, lineId }, c)));
|
|
28
30
|
}
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* The site's streams: every line under each codec's root, since lines on
|
|
33
|
+
* different protocols share one site NATS. A root alone means the generic
|
|
34
|
+
* layout; several codecs give the union of their subjects, in order.
|
|
35
|
+
*/
|
|
36
|
+
function lineStreamSpecs(codecs) {
|
|
37
|
+
const list = (Array.isArray(codecs) ? codecs : [codecs]).map(codecOf);
|
|
38
|
+
const union = (channel) => [...new Set(list.flatMap((c) => subjectsFor(c, c.channels[channel])))];
|
|
32
39
|
return [
|
|
33
|
-
{ name: exports.LINE_STREAM.EVENTS, subjects:
|
|
34
|
-
{ name: exports.LINE_STREAM.TELEMETRY, subjects:
|
|
40
|
+
{ name: exports.LINE_STREAM.EVENTS, subjects: union('events'), maxAgeMs: 30 * DAY_MS },
|
|
41
|
+
{ name: exports.LINE_STREAM.TELEMETRY, subjects: union('telemetry'), maxAgeMs: 3 * DAY_MS },
|
|
35
42
|
];
|
|
36
43
|
}
|
|
44
|
+
/** Whether a stream subject pattern keeps everything a consumer filter would read. */
|
|
45
|
+
function subjectCovers(stream, filter) {
|
|
46
|
+
const s = stream.split('.');
|
|
47
|
+
const f = filter.split('.');
|
|
48
|
+
for (let i = 0; i < s.length; i++) {
|
|
49
|
+
if (s[i] === '>')
|
|
50
|
+
return f.length > i;
|
|
51
|
+
if (i >= f.length)
|
|
52
|
+
return false;
|
|
53
|
+
if (f[i] === '>')
|
|
54
|
+
return false;
|
|
55
|
+
if (s[i] !== '*' && s[i] !== f[i])
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return s.length === f.length;
|
|
59
|
+
}
|
|
60
|
+
/** The consumer's filter subjects the stream keeps nothing of. */
|
|
61
|
+
function uncoveredSubjects(stream, filters) {
|
|
62
|
+
return filters.filter((f) => !stream.subjects.some((s) => subjectCovers(s, f)));
|
|
63
|
+
}
|
|
37
64
|
/** The telemetry subjects for one line, read live rather than replayed. */
|
|
38
65
|
function telemetrySubjects(codec, siteId, lineId) {
|
|
39
66
|
const c = codecOf(codec);
|
|
@@ -36,6 +36,9 @@ class MemoryStreams {
|
|
|
36
36
|
else
|
|
37
37
|
this.streams.set(spec.name, { spec, messages: [], seq: 0, consumers: new Map(), wake: new Set() });
|
|
38
38
|
}
|
|
39
|
+
async describeStream(name) {
|
|
40
|
+
return this.streams.get(name)?.spec ?? null;
|
|
41
|
+
}
|
|
39
42
|
async ensureConsumer(stream, spec) {
|
|
40
43
|
const s = this.stream(stream);
|
|
41
44
|
const existing = s.consumers.get(spec.name);
|
|
@@ -9,6 +9,8 @@ export declare function streamConfig(spec: StreamSpec): Partial<StreamConfig>;
|
|
|
9
9
|
export declare function consumerConfig(spec: ConsumerSpec): Partial<ConsumerConfig>;
|
|
10
10
|
/** Create the stream, or update an existing one to the spec. */
|
|
11
11
|
export declare function ensureStream(jsm: JetStreamManager, spec: StreamSpec): Promise<void>;
|
|
12
|
+
/** The stream as JetStream holds it, or null when there is none. */
|
|
13
|
+
export declare function describeStream(jsm: JetStreamManager, name: string): Promise<StreamSpec | null>;
|
|
12
14
|
/** Create the consumer, or move an existing one to the spec's subjects without losing its place. */
|
|
13
15
|
export declare function ensureConsumer(jsm: JetStreamManager, stream: string, spec: ConsumerSpec): Promise<void>;
|
|
14
16
|
export declare function payloadOf(m: {
|
|
@@ -20,6 +22,7 @@ export declare class NatsStreams implements LTStreams {
|
|
|
20
22
|
constructor(jsm: JetStreamManager, js: JetStreamClient);
|
|
21
23
|
ensureStream(spec: StreamSpec): Promise<void>;
|
|
22
24
|
ensureConsumer(stream: string, spec: ConsumerSpec): Promise<void>;
|
|
25
|
+
describeStream(name: string): Promise<StreamSpec | null>;
|
|
23
26
|
append(subject: Subject, payload: string): Promise<void>;
|
|
24
27
|
/** One message at a time: the next is not taken until the handler settles this one. */
|
|
25
28
|
consume(stream: string, consumer: string, handler: (m: LTStreamMessage) => Promise<Delivery>, opts?: LTConsumeOptions): Promise<LTSubscription>;
|
|
@@ -9,6 +9,7 @@ exports.NatsStreams = void 0;
|
|
|
9
9
|
exports.streamConfig = streamConfig;
|
|
10
10
|
exports.consumerConfig = consumerConfig;
|
|
11
11
|
exports.ensureStream = ensureStream;
|
|
12
|
+
exports.describeStream = describeStream;
|
|
12
13
|
exports.ensureConsumer = ensureConsumer;
|
|
13
14
|
exports.payloadOf = payloadOf;
|
|
14
15
|
const nats_1 = require("nats");
|
|
@@ -41,6 +42,18 @@ async function ensureStream(jsm, spec) {
|
|
|
41
42
|
}
|
|
42
43
|
await jsm.streams.update(spec.name, config);
|
|
43
44
|
}
|
|
45
|
+
/** The stream as JetStream holds it, or null when there is none. */
|
|
46
|
+
async function describeStream(jsm, name) {
|
|
47
|
+
try {
|
|
48
|
+
const info = await jsm.streams.info(name);
|
|
49
|
+
return { name, subjects: info.config.subjects ?? [], maxAgeMs: Math.round((info.config.max_age ?? 0) / 1_000_000) };
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
if (/stream not found/i.test(err?.message ?? ''))
|
|
53
|
+
return null;
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
44
57
|
/** Create the consumer, or move an existing one to the spec's subjects without losing its place. */
|
|
45
58
|
async function ensureConsumer(jsm, stream, spec) {
|
|
46
59
|
let existing;
|
|
@@ -72,6 +85,9 @@ class NatsStreams {
|
|
|
72
85
|
ensureConsumer(stream, spec) {
|
|
73
86
|
return ensureConsumer(this.jsm, stream, spec);
|
|
74
87
|
}
|
|
88
|
+
describeStream(name) {
|
|
89
|
+
return describeStream(this.jsm, name);
|
|
90
|
+
}
|
|
75
91
|
async append(subject, payload) {
|
|
76
92
|
await this.js.publish(subject, payload);
|
|
77
93
|
}
|
|
@@ -32,10 +32,12 @@ export declare class LocalStorageBackend implements StorageBackend {
|
|
|
32
32
|
directories: string[];
|
|
33
33
|
nextToken?: string;
|
|
34
34
|
}>;
|
|
35
|
+
/** The digest is read from the file itself, so it is always the stored bytes' own. */
|
|
35
36
|
getMetadata(key: string): Promise<{
|
|
36
37
|
size: number;
|
|
37
38
|
modified_at: string;
|
|
38
39
|
content_type: string;
|
|
40
|
+
sha256: string;
|
|
39
41
|
}>;
|
|
40
42
|
getSignedUrl(key: string, expiresInSeconds: number): Promise<string>;
|
|
41
43
|
}
|
|
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.LocalStorageBackend = void 0;
|
|
40
|
+
const crypto_1 = require("crypto");
|
|
40
41
|
const fs = __importStar(require("fs"));
|
|
41
42
|
const path = __importStar(require("path"));
|
|
42
43
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
@@ -168,6 +169,7 @@ class LocalStorageBackend {
|
|
|
168
169
|
nextToken: hasMore ? String(offset + limit) : undefined,
|
|
169
170
|
};
|
|
170
171
|
}
|
|
172
|
+
/** The digest is read from the file itself, so it is always the stored bytes' own. */
|
|
171
173
|
async getMetadata(key) {
|
|
172
174
|
const resolved = resolveAndValidate(key);
|
|
173
175
|
if (!fs.existsSync(resolved)) {
|
|
@@ -178,6 +180,7 @@ class LocalStorageBackend {
|
|
|
178
180
|
size: stat.size,
|
|
179
181
|
modified_at: stat.mtime.toISOString(),
|
|
180
182
|
content_type: (0, mime_1.mimeFromPath)(key),
|
|
183
|
+
sha256: (0, crypto_1.createHash)('sha256').update(fs.readFileSync(resolved)).digest('hex'),
|
|
181
184
|
};
|
|
182
185
|
}
|
|
183
186
|
async getSignedUrl(key, expiresInSeconds) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StorageBackend } from './types';
|
|
1
|
+
import type { StorageBackend, WriteOptions } from './types';
|
|
2
2
|
export declare class S3StorageBackend implements StorageBackend {
|
|
3
3
|
private client;
|
|
4
4
|
private signingClient;
|
|
@@ -6,7 +6,7 @@ export declare class S3StorageBackend implements StorageBackend {
|
|
|
6
6
|
private bucketReady;
|
|
7
7
|
constructor();
|
|
8
8
|
private ensureBucket;
|
|
9
|
-
write(key: string, data: Buffer): Promise<{
|
|
9
|
+
write(key: string, data: Buffer, options?: WriteOptions): Promise<{
|
|
10
10
|
ref: string;
|
|
11
11
|
size: number;
|
|
12
12
|
}>;
|
|
@@ -42,6 +42,7 @@ export declare class S3StorageBackend implements StorageBackend {
|
|
|
42
42
|
size: number;
|
|
43
43
|
modified_at: string;
|
|
44
44
|
content_type: string;
|
|
45
|
+
sha256?: string;
|
|
45
46
|
}>;
|
|
46
47
|
getSignedUrl(key: string, expiresInSeconds: number): Promise<string>;
|
|
47
48
|
}
|
package/build/lib/storage/s3.js
CHANGED
|
@@ -39,6 +39,8 @@ const os = __importStar(require("os"));
|
|
|
39
39
|
const path = __importStar(require("path"));
|
|
40
40
|
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
41
41
|
const s3_request_presigner_1 = require("@aws-sdk/s3-request-presigner");
|
|
42
|
+
/** The object metadata key the digest is kept under. */
|
|
43
|
+
const SHA256_METADATA = 'sha256';
|
|
42
44
|
const mime_1 = require("./mime");
|
|
43
45
|
const STAGING_DIR = path.join(os.tmpdir(), 'lt-staging');
|
|
44
46
|
class S3StorageBackend {
|
|
@@ -91,12 +93,13 @@ class S3StorageBackend {
|
|
|
91
93
|
}
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
|
-
async write(key, data) {
|
|
96
|
+
async write(key, data, options) {
|
|
95
97
|
await this.bucketReady;
|
|
96
98
|
await this.client.send(new client_s3_1.PutObjectCommand({
|
|
97
99
|
Bucket: this.bucket,
|
|
98
100
|
Key: normalizeKey(key),
|
|
99
101
|
Body: data,
|
|
102
|
+
...(options?.sha256 ? { Metadata: { [SHA256_METADATA]: options.sha256 } } : {}),
|
|
100
103
|
}));
|
|
101
104
|
return { ref: key, size: data.length };
|
|
102
105
|
}
|
|
@@ -222,10 +225,12 @@ class S3StorageBackend {
|
|
|
222
225
|
Bucket: this.bucket,
|
|
223
226
|
Key: normalizeKey(key),
|
|
224
227
|
}));
|
|
228
|
+
const sha256 = resp.Metadata?.[SHA256_METADATA];
|
|
225
229
|
return {
|
|
226
230
|
size: resp.ContentLength || 0,
|
|
227
231
|
modified_at: resp.LastModified?.toISOString() || new Date().toISOString(),
|
|
228
232
|
content_type: resp.ContentType || (0, mime_1.mimeFromPath)(key),
|
|
233
|
+
...(sha256 ? { sha256 } : {}),
|
|
229
234
|
};
|
|
230
235
|
}
|
|
231
236
|
async getSignedUrl(key, expiresInSeconds) {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
export interface WriteOptions {
|
|
2
|
+
/** The SHA-256 of `data`, hex, kept with the object. */
|
|
3
|
+
sha256?: string;
|
|
4
|
+
}
|
|
1
5
|
export interface StorageBackend {
|
|
2
6
|
/** Write data to storage. Returns the key and size. */
|
|
3
|
-
write(key: string, data: Buffer): Promise<{
|
|
7
|
+
write(key: string, data: Buffer, options?: WriteOptions): Promise<{
|
|
4
8
|
ref: string;
|
|
5
9
|
size: number;
|
|
6
10
|
}>;
|
|
@@ -53,6 +57,8 @@ export interface StorageBackend {
|
|
|
53
57
|
size: number;
|
|
54
58
|
modified_at: string;
|
|
55
59
|
content_type: string;
|
|
60
|
+
/** The object's SHA-256, hex, when the backend knows it. */
|
|
61
|
+
sha256?: string;
|
|
56
62
|
}>;
|
|
57
63
|
/** Generate a time-limited signed URL for unauthenticated file access. */
|
|
58
64
|
getSignedUrl(key: string, expiresInSeconds: number): Promise<string>;
|