@ethernity-cloud/sdk-js 1.5.4 → 1.5.8

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.
@@ -54,13 +54,143 @@ let _provider = null;
54
54
  * Wire the registry to the enclave's identity, storage and chain access.
55
55
  * Called by securelock at task start; payload code never calls this.
56
56
  */
57
- function configure({ identityPriv, swiftStreamService, bucket, contractAddress, provider }) {
57
+ function configure({ identityPriv, swiftStreamService, bucket, contractAddress, provider, caller = null }) {
58
58
  _identityPriv = identityPriv;
59
59
  _swift = swiftStreamService;
60
60
  _bucket = bucket;
61
61
  _contractAddress = contractAddress;
62
62
  _provider = provider;
63
63
  _esrLedger = [];
64
+ _taskLedger = {};
65
+ _taskCaller = normAddr(caller);
66
+ }
67
+
68
+ // The authenticated task caller: the wallet that placed the DO request, read
69
+ // from the PoX contract by the TRUSTEDZONE and forwarded over its signed
70
+ // handoff (caller.securelock + .sig, verified by securelock before configure).
71
+ // null when the trustedzone did not supply one -- anonymity is never a
72
+ // privilege: null cannot claim ownership or touch owned state.
73
+ let _taskCaller = null;
74
+
75
+ function taskCaller() {
76
+ return _taskCaller;
77
+ }
78
+
79
+ function normAddr(addr) {
80
+ return typeof addr === 'string' && addr ? addr.toLowerCase() : null;
81
+ }
82
+
83
+ /**
84
+ * State container + ACL. Stored blobs are either legacy (raw state --
85
+ * "unowned") or an owned container:
86
+ * { _ecld_state: 1, acl: {...}, data: <state> }
87
+ * acl = { owner, read: [], write: [], public_read: false }. Enforcement
88
+ * happens HERE, inside the enclave, against the trustedzone-attested caller.
89
+ */
90
+ const CONTAINER_MARK = '_ecld_state';
91
+
92
+ class StatePermissionError extends Error {}
93
+
94
+ function unwrapStored(stored) {
95
+ if (stored && typeof stored === 'object' && stored[CONTAINER_MARK] === 1) {
96
+ return [stored.acl || null, stored.data];
97
+ }
98
+ return [null, stored];
99
+ }
100
+
101
+ function wrapStored(acl, data, committedBy = null) {
102
+ // For owned state, bind the authoring caller (committedBy) INTO the blob, so
103
+ // it lands in the CID -- the field the securelock signs -- making the caller
104
+ // cryptographically bound to the commit, not merely a re-stampable sidecar.
105
+ if (!acl) return data; // unowned keeps the legacy shape
106
+ const container = { [CONTAINER_MARK]: 1, acl, data };
107
+ if (committedBy) container.committedBy = committedBy;
108
+ return container;
109
+ }
110
+
111
+ /**
112
+ * Post-payload: re-assert the trusted caller across the ESR ledger. Called by
113
+ * the securelock after the payload returns, with the attested caller captured
114
+ * before execution (outside the payload's reach). Overwrites callerUsed (and
115
+ * re-fixes enclave to the real signer) in every owned entry and RE-STAGES the
116
+ * ledger, so a forged callerUsed cannot reach the trustedzone. Returns true if
117
+ * any entry's callerUsed differed from the trusted value (tamper detected).
118
+ * Defence in depth -- the trustedzone and the CID-bound caller are the sound
119
+ * layers; this runs in the same process the payload subverted.
120
+ */
121
+ async function restampLedgerCaller(trustedCaller) {
122
+ const trusted = normAddr(trustedCaller);
123
+ if (!_esrLedger.length) return false;
124
+ let tampered = false;
125
+ let signerAddr = null;
126
+ try {
127
+ signerAddr = await new StateRegistry()._signer().getAddress();
128
+ } catch (e) { signerAddr = null; }
129
+ for (const entry of _esrLedger) {
130
+ if (!entry.owned) continue;
131
+ if (normAddr(entry.callerUsed) !== trusted) tampered = true;
132
+ entry.callerUsed = trusted || '';
133
+ if (signerAddr) entry.enclave = signerAddr;
134
+ }
135
+ try {
136
+ await _swift.putFileContent(
137
+ _bucket, 'esr.authorizations.json', '',
138
+ Buffer.from(JSON.stringify(_esrLedger), 'utf8'));
139
+ } catch (e) { /* best-effort restage; trustedzone still adjudicates */ }
140
+ return tampered;
141
+ }
142
+
143
+ function aclMembers(acl, field) {
144
+ return new Set((acl[field] || []).map(normAddr).filter(Boolean));
145
+ }
146
+
147
+ function canRead(acl) {
148
+ if (!acl || acl.public_read) return true;
149
+ const c = _taskCaller;
150
+ if (!c) return false;
151
+ return c === normAddr(acl.owner) || aclMembers(acl, 'read').has(c) || aclMembers(acl, 'write').has(c);
152
+ }
153
+
154
+ function canWrite(acl) {
155
+ if (!acl) return true; // unowned: writable; claimed when a caller exists
156
+ const c = _taskCaller;
157
+ if (!c) return false;
158
+ return c === normAddr(acl.owner) || aclMembers(acl, 'write').has(c);
159
+ }
160
+
161
+ function newAcl(owner) {
162
+ return { owner, read: [], write: [], public_read: false };
163
+ }
164
+
165
+ // Task-scoped ledger of every state key this execution touched, recorded by
166
+ // get()/commit(). ecldResult() snapshots it to attach fresh state to the task
167
+ // result, so callers (and the runner's state cache) get current state in the
168
+ // same result -- no separate read task needed. Reset per task in configure().
169
+ let _taskLedger = {};
170
+
171
+ function ledgerRecord(key, version, cid, state) {
172
+ _taskLedger[key] = { key, version: Number(version), cid, state };
173
+ }
174
+
175
+ /**
176
+ * The `esr` attachment for ecldResult: wallet + entries. Synchronous by
177
+ * design (the securelock consumes results synchronously): entries come from
178
+ * the in-memory ledger of keys this task already touched. To attach a key the
179
+ * task did not touch, use the async esrFetch() helper, which awaits the reads
180
+ * (populating the ledger) before building the result.
181
+ */
182
+ function ledgerSnapshot(includeState = true, keys = null) {
183
+ const reg = new StateRegistry();
184
+ const wanted = keys ? keys : Object.keys(_taskLedger);
185
+ const entries = [];
186
+ for (const k of wanted) {
187
+ const e = _taskLedger[k];
188
+ if (!e) continue;
189
+ const entry = { ...e };
190
+ if (!includeState) delete entry.state;
191
+ entries.push(entry);
192
+ }
193
+ return { wallet: reg.walletAddress, entries };
64
194
  }
65
195
 
66
196
  function keccak256(buf) {
@@ -215,8 +345,23 @@ class StateRegistry {
215
345
  * state" would let the next commit overwrite data that is still recoverable.
216
346
  */
217
347
  async get(key, fallback = {}) {
348
+ const [acl, data, version, cid] = await this._readContainer(key, fallback);
349
+ if (!canRead(acl)) {
350
+ throw new StatePermissionError(
351
+ `caller ${_taskCaller || '<anonymous>'} has no read permission on ` +
352
+ `state key '${key}' (owner: ${acl.owner})`
353
+ );
354
+ }
355
+ ledgerRecord(key, version || 0, cid, data);
356
+ return data;
357
+ }
358
+
359
+ /** [acl, data, version, cid] for `key`; NO permission check here. */
360
+ async _readContainer(key, fallback = {}) {
218
361
  const [cid, version] = await this._contract().getState(this.walletAddress, keyHash(key));
219
- if (!version || !cid) return fallback;
362
+ if (!version || !cid) {
363
+ return [null, fallback, 0, null];
364
+ }
220
365
  if (!looksLikeCID(cid)) {
221
366
  throw new Error(
222
367
  `ESR entry for '${key}' holds a pointer that is not a CID (${cid.slice(0, 32)}…). ` +
@@ -224,7 +369,9 @@ class StateRegistry {
224
369
  );
225
370
  }
226
371
  const blob = await this._fetch(key, cid);
227
- return JSON.parse(decrypt(blob).toString('utf8'));
372
+ const stored = JSON.parse(decrypt(blob).toString('utf8'));
373
+ const [acl, data] = unwrapStored(stored);
374
+ return [acl, data, Number(version), cid];
228
375
  }
229
376
 
230
377
  /**
@@ -235,15 +382,39 @@ class StateRegistry {
235
382
  * retry, so concurrent tasks cannot silently lose updates.
236
383
  */
237
384
  async commit(key, mutate, attempts = 3) {
385
+ const transform = async (acl, data) => {
386
+ if (!canWrite(acl)) {
387
+ throw new StatePermissionError(
388
+ `caller ${_taskCaller || '<anonymous>'} has no write permission on ` +
389
+ `state key '${key}' (owner: ${acl.owner})`
390
+ );
391
+ }
392
+ let nextAcl = acl;
393
+ if (!nextAcl && _taskCaller) nextAcl = newAcl(_taskCaller); // first-writer-owns
394
+ return [nextAcl, await mutate(data)];
395
+ };
396
+ const [, newData] = await this._commitTransform(key, transform, attempts);
397
+ return newData;
398
+ }
399
+
400
+ /**
401
+ * Optimistic-concurrency commit of transform(acl, data, version) ->
402
+ * [newAcl, newData]. The stored blob is the wrapped container (or the bare
403
+ * data while unowned). On a version race we re-read and retry, so
404
+ * concurrent tasks cannot silently lose updates.
405
+ */
406
+ async _commitTransform(key, transform, attempts = 3) {
238
407
  let lastError = null;
239
408
  for (let attempt = 0; attempt < attempts; attempt += 1) {
240
409
  // eslint-disable-next-line no-await-in-loop
241
- const currentVersion = await this.getVersion(key);
410
+ const [acl, data, currentVersion] = await this._readContainer(key);
242
411
  // eslint-disable-next-line no-await-in-loop
243
- const current = currentVersion ? await this.get(key) : {};
244
- const newState = await mutate(current);
412
+ const [nextAcl, newData] = await transform(acl, data, currentVersion);
413
+ // Bind the authoring caller into the blob (hence the CID/signature) for
414
+ // owned state.
415
+ const stored = wrapStored(nextAcl, newData, nextAcl ? _taskCaller : null);
245
416
 
246
- const blob = encrypt(Buffer.from(JSON.stringify(newState), 'utf8'));
417
+ const blob = encrypt(Buffer.from(JSON.stringify(stored), 'utf8'));
247
418
  // Compute the CID from OUR bytes — never accept one from the node.
248
419
  const cid = cidv1Raw(blob);
249
420
 
@@ -254,8 +425,12 @@ class StateRegistry {
254
425
 
255
426
  try {
256
427
  // eslint-disable-next-line no-await-in-loop
257
- await this._sendCommit(keyHash(key), cid, currentVersion);
258
- return newState;
428
+ await this._sendCommit(keyHash(key), cid, currentVersion,
429
+ nextAcl ? normAddr(nextAcl.owner) : null);
430
+ // Record the POST-commit values (the DATA -- the ACL never leaves in
431
+ // results): this is what the chain shows once the relay lands.
432
+ ledgerRecord(key, currentVersion + 1, cid, newData);
433
+ return [nextAcl, newData];
259
434
  } catch (e) {
260
435
  const msg = String(e && e.message ? e.message : e);
261
436
  if (/VersionMismatch/i.test(msg) || /version/i.test(msg)) {
@@ -299,7 +474,7 @@ class StateRegistry {
299
474
  throw new Error(`Could not read state object for '${key}' (${cid})`);
300
475
  }
301
476
 
302
- async _sendCommit(keyHashHex, cid, expectedVersion) {
477
+ async _sendCommit(keyHashHex, cid, expectedVersion, ownerAddr = null) {
303
478
  // The enclave never pays gas and does NO gas math: it SIGNS the commit
304
479
  // (commitFor) and stages the signed authorization for the node, which
305
480
  // relays it and pays. The trustedzone independently re-prices the whole
@@ -322,7 +497,15 @@ class StateRegistry {
322
497
  cid,
323
498
  expectedVersion: Number(expectedVersion),
324
499
  relayNonce: Number(relayNonce),
325
- signature
500
+ signature,
501
+ // IMPERSONATION GUARD: the trustedzone requires callerUsed to equal the
502
+ // DO owner it reads from the PoX contract, so a payload that forged the
503
+ // in-enclave caller to impersonate another user is rejected outside the
504
+ // payload's process. It does NOT verify the caller was a permitted
505
+ // writer (it cannot read the encrypted ACL). Unowned commits carry no
506
+ // constraint.
507
+ callerUsed: _taskCaller || '',
508
+ owned: Boolean(ownerAddr)
326
509
  };
327
510
 
328
511
  _esrLedger.push(auth);
@@ -336,9 +519,100 @@ class StateRegistry {
336
519
  }
337
520
  }
338
521
 
522
+ /** Owner-only ACL mutation; an unowned key is claimed first (owner = caller). */
523
+ async function ownerOp(key, mutator) {
524
+ if (!_taskCaller) {
525
+ throw new StatePermissionError(
526
+ 'state management requires an authenticated caller (the trustedzone supplied none)'
527
+ );
528
+ }
529
+ const reg = new StateRegistry();
530
+ const transform = async (acl, data) => {
531
+ let nextAcl = acl || newAcl(_taskCaller);
532
+ if (normAddr(nextAcl.owner) !== _taskCaller) {
533
+ throw new StatePermissionError(
534
+ `caller ${_taskCaller} is not the owner of state key '${key}' (owner: ${nextAcl.owner})`
535
+ );
536
+ }
537
+ nextAcl = mutator({ ...nextAcl });
538
+ return [nextAcl, data];
539
+ };
540
+ const [acl] = await reg._commitTransform(key, transform);
541
+ return { ...acl };
542
+ }
543
+
544
+ function assertLevel(level) {
545
+ if (level !== 'read' && level !== 'write') throw new Error("level must be 'read' or 'write'");
546
+ }
547
+
548
+ async function esrGrant(key, address, level = 'read') {
549
+ assertLevel(level);
550
+ const addr = normAddr(address);
551
+ if (!addr) throw new Error('a grantee address is required');
552
+ return ownerOp(key, (acl) => {
553
+ const members = new Set((acl[level] || []).map(normAddr).filter(Boolean));
554
+ members.add(addr);
555
+ acl[level] = [...members].sort();
556
+ return acl;
557
+ });
558
+ }
559
+
560
+ async function esrRevoke(key, address, level = 'read') {
561
+ assertLevel(level);
562
+ const addr = normAddr(address);
563
+ return ownerOp(key, (acl) => {
564
+ acl[level] = (acl[level] || []).map(normAddr).filter((a) => a && a !== addr).sort();
565
+ return acl;
566
+ });
567
+ }
568
+
569
+ async function esrSetPublicRead(key, enabled = true) {
570
+ return ownerOp(key, (acl) => {
571
+ acl.public_read = Boolean(enabled);
572
+ return acl;
573
+ });
574
+ }
575
+
576
+ async function esrTransfer(key, newOwner) {
577
+ const addr = normAddr(newOwner);
578
+ if (!addr) throw new Error('a new owner address is required');
579
+ return ownerOp(key, (acl) => {
580
+ acl.owner = addr;
581
+ return acl;
582
+ });
583
+ }
584
+
585
+ /** Owner address of `key`, or null while unowned. Exposes nothing else. */
586
+ async function esrOwner(key) {
587
+ const [acl] = await new StateRegistry()._readContainer(key);
588
+ return acl ? acl.owner : null;
589
+ }
590
+
591
+ /** Owner-only: the full ACL of `key`. */
592
+ async function esrAcl(key) {
593
+ const [acl] = await new StateRegistry()._readContainer(key);
594
+ if (!acl) return null;
595
+ if (!_taskCaller || normAddr(acl.owner) !== _taskCaller) {
596
+ throw new StatePermissionError(
597
+ `caller ${_taskCaller || '<anonymous>'} is not the owner of state key '${key}'`
598
+ );
599
+ }
600
+ return { ...acl };
601
+ }
602
+
339
603
  module.exports = {
340
604
  StateRegistry,
341
605
  configure,
342
606
  cidv1Raw,
343
- looksLikeCID
607
+ looksLikeCID,
608
+ ledgerSnapshot,
609
+ taskCaller,
610
+ restampLedgerCaller,
611
+ StatePermissionError,
612
+ esrGrant,
613
+ esrRevoke,
614
+ esrSetPublicRead,
615
+ esrTransfer,
616
+ esrOwner,
617
+ esrAcl
344
618
  };
@@ -30,8 +30,84 @@ try {
30
30
  }
31
31
  }
32
32
 
33
+ /**
34
+ * Automatic encoding for task results (single code path for all results):
35
+ * JSON-serializable values ride as JSON, strings as text, bytes-like as
36
+ * base64. Anything else is JSON-stringified with a String() fallback -- a
37
+ * task result never fails on encoding.
38
+ */
39
+ function encodeResultData(data) {
40
+ if (Buffer.isBuffer(data) || data instanceof Uint8Array) {
41
+ return ['base64', Buffer.from(data).toString('base64')];
42
+ }
43
+ if (typeof data === 'string') {
44
+ return ['text', data];
45
+ }
46
+ try {
47
+ JSON.stringify(data);
48
+ return ['json', data === undefined ? null : data];
49
+ } catch (e) {
50
+ try {
51
+ return ['json', JSON.parse(JSON.stringify(data, (k, v) =>
52
+ typeof v === 'bigint' || typeof v === 'function' ? String(v) : v))];
53
+ } catch (e2) {
54
+ return ['text', String(data)];
55
+ }
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Return a task result (THE way to end a task).
61
+ *
62
+ * Builds the structured result envelope:
63
+ * { ecld: 1, type: 'json'|'text'|'base64', data: ..., esr: ... }
64
+ *
65
+ * - `data` is encoded automatically (see encodeResultData).
66
+ * - `state: true` (default) attaches the ESR state of every key this task
67
+ * touched; `state: 'meta'` attaches {key, version, cid} without the state
68
+ * payload; `state: false` attaches nothing.
69
+ * - `keys: [...]` restricts the attachment to those keys (already-touched
70
+ * only -- to attach an untouched key, use the async esrFetch helper).
71
+ *
72
+ * ESR attachment is best-effort: an ESR-disabled build (or any attachment
73
+ * error) yields esr: null, never a failed task.
74
+ */
75
+ function ecldResult(data = null, { state = true, keys = null } = {}) {
76
+ const [type, encoded] = encodeResultData(data);
77
+ const envelope = { ecld: 1, type, data: encoded, esr: null };
78
+ if (state || keys) {
79
+ try {
80
+ // eslint-disable-next-line global-require
81
+ const ecldState = require('./ecld_state');
82
+ envelope.esr = ecldState.ledgerSnapshot(state === true, keys);
83
+ } catch (e) {
84
+ envelope.esr = null;
85
+ }
86
+ }
87
+ return [0, JSON.stringify(envelope)];
88
+ }
89
+
90
+ /** Legacy alias: same builder, ESR attachment off -- today's behavior. */
33
91
  function ___etny_result___(data) {
34
- return [0, data];
92
+ return ecldResult(data, { state: false });
93
+ }
94
+
95
+ /**
96
+ * Standard state-fetch task body: awaits the reads (populating the task
97
+ * ledger), then attaches them. The runner's cache-gated read submits this on
98
+ * a cache miss: `await esrFetch('profile-7')`.
99
+ */
100
+ async function esrFetch(...keys) {
101
+ try {
102
+ // eslint-disable-next-line global-require
103
+ const { StateRegistry } = require('./ecld_state');
104
+ const reg = new StateRegistry();
105
+ for (const k of keys) {
106
+ // eslint-disable-next-line no-await-in-loop
107
+ await reg.get(k);
108
+ }
109
+ } catch (e) { /* ESR-disabled build: esr stays null */ }
110
+ return ecldResult(null, { keys: keys.length ? keys : null });
35
111
  }
36
112
 
37
113
  // Names of *_ADDRESS-style enclave config vars that are set but EMPTY.
@@ -66,7 +142,23 @@ function executeTask(payload, input) {
66
142
  ' image dependencies, or a bad require() inside backend.js.'
67
143
  ];
68
144
  }
69
- return exec(payload, input, { '___etny_result___': ___etny_result___, ...backendFunctions });
145
+ const scope = {
146
+ '___etny_result___': ___etny_result___, // legacy alias (no ESR)
147
+ 'ecldResult': ecldResult, // the result API
148
+ 'esrFetch': esrFetch, // standard state-fetch task
149
+ ...backendFunctions,
150
+ };
151
+ // State ownership / ACL API (present only in ESR-enabled builds). Enforced
152
+ // inside ecld_state against the trustedzone-attested caller.
153
+ try {
154
+ // eslint-disable-next-line global-require
155
+ const ecldState = require('./ecld_state');
156
+ for (const name of ['taskCaller', 'esrGrant', 'esrRevoke',
157
+ 'esrSetPublicRead', 'esrTransfer', 'esrOwner', 'esrAcl']) {
158
+ if (!(name in scope)) scope[name] = ecldState[name];
159
+ }
160
+ } catch (e) { /* non-ESR build */ }
161
+ return exec(payload, input, scope);
70
162
  }
71
163
 
72
164
  function exec(payload, input, globals = null) {
@@ -80,7 +172,15 @@ function exec(payload, input, globals = null) {
80
172
  // the payload by bare name — matching how payloads are written
81
173
  // (e.g. `processData(___etny_data_set___)`).
82
174
  with (scope) {
83
- return ___etny_result___(eval(payload));
175
+ const value = eval(payload);
176
+ // A payload that called ecldResult/___etny_result___ itself
177
+ // already holds the finished [0, envelopeJson] tuple -- pass
178
+ // it through instead of wrapping the result a second time.
179
+ if (Array.isArray(value) && value.length === 2 &&
180
+ value[0] === 0 && typeof value[1] === 'string') {
181
+ return value;
182
+ }
183
+ return ___etny_result___(value);
84
184
  }
85
185
  } else {
86
186
  return [TaskStatus.PAYLOAD_NOT_DEFINED, 'Could not find the source file to execute'];
@@ -436,6 +436,7 @@ class EtnySecureLock {
436
436
  bucket: this.etny_bucket,
437
437
  contractAddress: contractAddress,
438
438
  provider: this.w3,
439
+ caller: this.task_caller || null,
439
440
  });
440
441
  console.log(`StateRegistry ready (ESR ${contractAddress})`);
441
442
  } catch (e) {
@@ -456,13 +457,58 @@ class EtnySecureLock {
456
457
  return false;
457
458
  }
458
459
 
460
+ // Attested task caller: the DO-request owner, forwarded by the
461
+ // trustedzone as caller.securelock + .sig. Accepted ONLY with a valid
462
+ // trustedzone signature -- this build's crypto module has no verify
463
+ // helper yet, so until one lands the caller stays unset (anonymous)
464
+ // rather than trusting an unverifiable value.
465
+ this.task_caller = null;
466
+ try {
467
+ const callerValue = await this.getFileContentAndDecrypt(this.etny_bucket, 'caller.securelock');
468
+ const callerSig = await this.getFileContentAndDecrypt(this.etny_bucket, 'caller.securelock.sig');
469
+ if (typeof etny_crypto.verifySignature === 'function') {
470
+ await this.getTrustedZonePublicKey();
471
+ if (etny_crypto.verifySignature(this.trustedZonePublicKey, callerValue, callerSig)) {
472
+ this.task_caller = String(callerValue).trim();
473
+ console.log(`Task caller attested by trustedzone: ${this.task_caller}`);
474
+ } else {
475
+ console.error('caller.securelock signature validation FAILED; treating task as anonymous');
476
+ }
477
+ } else {
478
+ console.log('No signature-verify support in this build; ignoring caller.securelock (anonymous)');
479
+ }
480
+ } catch (e) {
481
+ console.log('No attested task caller supplied; treating task as anonymous');
482
+ }
483
+
459
484
  this.configureStateRegistry();
460
485
 
461
- const task_result = executeTask(payload_data, input_data);
486
+ const task_result = await executeTask(payload_data, input_data);
462
487
  console.log('TASK RESULT (code, data):', task_result);
463
488
  this.task_code = task_result[0];
464
489
  this.task_result = task_result[1];
465
490
 
491
+ // Post-payload: re-assert the attested caller across the ESR ledger from
492
+ // THIS securelock's private copy (this.task_caller), captured before the
493
+ // payload ran and never handed to the payload. Corrects any callerUsed
494
+ // the payload may have forged (so the trustedzone reads the truth) and
495
+ // fails the task locally if a forgery is detected. Defence in depth;
496
+ // the trustedzone (separate enclave) and the CID-bound caller remain the
497
+ // authoritative layers.
498
+ try {
499
+ const ecld_state = require("./ecld_state");
500
+ if (typeof ecld_state.restampLedgerCaller === 'function') {
501
+ const tampered = await ecld_state.restampLedgerCaller(this.task_caller || null);
502
+ if (tampered) {
503
+ console.error('ESR ledger caller was tampered with by the payload -- failing the task (SECURITY_VIOLATION).');
504
+ this.task_code = TaskStatus.SECURITY_VIOLATION;
505
+ this.task_result = 'SECURITY_VIOLATION: the task attempted to forge the state commit caller. The attested submitter was re-asserted and the task was terminated.';
506
+ }
507
+ }
508
+ } catch (e) {
509
+ console.error('ESR ledger re-stamp skipped:', e && e.message ? e.message : e);
510
+ }
511
+
466
512
  return true;
467
513
  }
468
514
 
@@ -17,6 +17,10 @@ class TaskStatus {
17
17
  static CONFIG_ERROR = 32;
18
18
  static EXECUTION_TIMEOUT = 33; // Started but produced no result within the order duration.
19
19
  static ESR_GAS_LIMIT_EXCEEDED = 34; // ESR commits would exceed the per-order relayed-gas budget.
20
+ static SECURITY_VIOLATION = 35; // A state commit was authorized under a caller other than
21
+ // the task's submitter (the in-enclave ownership check was
22
+ // bypassed). Set by the securelock on a detected forged
23
+ // caller, and by the trustedzone re-adjudication.
20
24
  }
21
25
 
22
26
  module.exports = {
@@ -115,6 +115,36 @@ try {
115
115
  process.exit(1);
116
116
  }
117
117
 
118
+ // Safety lint: flag dynamic code execution -- especially of task input --
119
+ // before the image is sealed. A hard error only for the case that actually
120
+ // opens the enclave to a submitter (eval / Function of ___etny_data_set___);
121
+ // everything else is a warning. See payloadLint.mjs for scope + opt-out.
122
+ try {
123
+ const { analyze } = await import('../payloadLint.mjs');
124
+ const src = fs.readFileSync(backendFile, 'utf8');
125
+ const { findings, optedOut } = analyze(src, 'src/serverless/backend.js');
126
+ if (!optedOut) {
127
+ const warnings = findings.filter((f) => f.severity === 'warning');
128
+ const errors = findings.filter((f) => f.severity === 'error');
129
+ for (const f of warnings) {
130
+ console.warn(`WARNING: src/serverless/backend.js:${f.line}: ${f.message}`);
131
+ }
132
+ if (errors.length) {
133
+ console.error('');
134
+ console.error('ERROR: unsafe dynamic execution of task input in src/serverless/backend.js:');
135
+ for (const f of errors) console.error(` line ${f.line}: ${f.message}`);
136
+ console.error('');
137
+ console.error(' This would let a task submitter run arbitrary code inside your enclave');
138
+ console.error(' and reach other users\' state. Fix it, or if the input is genuinely');
139
+ console.error(' trusted, add `// ecld: allow-eval` on that line to acknowledge the risk.');
140
+ process.exit(1);
141
+ }
142
+ }
143
+ } catch (e) {
144
+ // The lint is best-effort; never block a build on the linter itself failing.
145
+ console.warn(`WARNING: payload safety lint skipped (${e && e.message ? e.message : e})`);
146
+ }
147
+
118
148
  console.log(`Creating destination directory: ${destDir}`);
119
149
  fs.mkdirSync(destDir, { recursive: true });
120
150
 
package/package.json CHANGED
@@ -1,50 +1,51 @@
1
- {
2
- "name": "@ethernity-cloud/sdk-js",
3
- "version": "1.5.4",
4
- "description": "A package to run ethernity cloud build scripts",
5
- "scripts": {
6
- "ecld-build": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/build.js",
7
- "ecld-publish": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/publish.js",
8
- "ecld-init": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/init.js",
9
- "ecld-run": "cross-env ecld-run",
10
- "postinstall": "node postinstall.js",
11
- "ecld-test": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/test.js"
12
- },
13
- "bin": {
14
- "ecld-build": "./build.js",
15
- "ecld-publish": "./publish.js",
16
- "ecld-init": "./init.js",
17
- "postinstall": "./postinstall.js",
18
- "ecld-test": "./test.js"
19
- },
20
- "files": [
21
- "nodenithy/*",
22
- "pynithy/*",
23
- "build.js",
24
- "init.js",
25
- "publish.js",
26
- "postinstall.js"
27
- ],
28
- "dependencies": {
29
- "@ethernity-cloud/runner": "^0.0.26",
30
- "axios": "^1.7.2",
31
- "commander": "^12.1.0",
32
- "dotenv": "^16.4.5",
33
- "eth-lib": "^0.1.29",
34
- "ethers": "^5.5.4",
35
- "kubo-rpc-client": "^4.1.1",
36
- "web3": "^4.9.0",
37
- "cross-env": "^7.0.3",
38
- "shelljs": "^0.8.5",
39
- "shx": "^0.3.4",
40
- "node-forge": "^1.3.1",
41
- "cli-progress": "^3.12.0",
42
- "react": "^18.3.1",
43
- "react-dom": "^18.3.1",
44
- "react-scripts": "^5.0.1",
45
- "web-vitals": "^2.1.4",
46
- "adm-zip": "^0.5.14"
47
- },
48
- "author": "Alexandru Luca",
49
- "license": "AGPL-3.0"
50
- }
1
+ {
2
+ "name": "@ethernity-cloud/sdk-js",
3
+ "version": "1.5.8",
4
+ "description": "A package to run ethernity cloud build scripts",
5
+ "scripts": {
6
+ "ecld-build": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/build.js",
7
+ "ecld-publish": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/publish.js",
8
+ "ecld-init": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/init.js",
9
+ "ecld-run": "cross-env ecld-run",
10
+ "postinstall": "node postinstall.js",
11
+ "ecld-test": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/test.js"
12
+ },
13
+ "bin": {
14
+ "ecld-build": "./build.js",
15
+ "ecld-publish": "./publish.js",
16
+ "ecld-init": "./init.js",
17
+ "postinstall": "./postinstall.js",
18
+ "ecld-test": "./test.js"
19
+ },
20
+ "files": [
21
+ "nodenithy/*",
22
+ "pynithy/*",
23
+ "build.js",
24
+ "init.js",
25
+ "publish.js",
26
+ "postinstall.js"
27
+ ],
28
+ "dependencies": {
29
+ "@ethernity-cloud/runner": "^0.0.26",
30
+ "axios": "^1.7.2",
31
+ "commander": "^12.1.0",
32
+ "dotenv": "^16.4.5",
33
+ "eth-lib": "^0.1.29",
34
+ "ethers": "^5.5.4",
35
+ "kubo-rpc-client": "^4.1.1",
36
+ "web3": "^4.9.0",
37
+ "cross-env": "^7.0.3",
38
+ "shelljs": "^0.8.5",
39
+ "shx": "^0.3.4",
40
+ "node-forge": "^1.3.1",
41
+ "cli-progress": "^3.12.0",
42
+ "react": "^18.3.1",
43
+ "react-dom": "^18.3.1",
44
+ "react-scripts": "^5.0.1",
45
+ "web-vitals": "^2.1.4",
46
+ "adm-zip": "^0.5.14",
47
+ "acorn": "^8.11.0"
48
+ },
49
+ "author": "Alexandru Luca",
50
+ "license": "AGPL-3.0"
51
+ }