@teleporthq/teleport-plugin-next-workflows 0.44.6 → 0.44.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.
Files changed (67) hide show
  1. package/__tests__/credential-columns-reply.test.ts +286 -0
  2. package/__tests__/credential-result-reply.test.ts +509 -0
  3. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  4. package/dist/cjs/api-route-generator.js +6 -2
  5. package/dist/cjs/api-route-generator.js.map +1 -1
  6. package/dist/cjs/credential-columns-code.d.ts +17 -0
  7. package/dist/cjs/credential-columns-code.d.ts.map +1 -0
  8. package/dist/cjs/credential-columns-code.js +61 -0
  9. package/dist/cjs/credential-columns-code.js.map +1 -0
  10. package/dist/cjs/credential-results.d.ts +18 -0
  11. package/dist/cjs/credential-results.d.ts.map +1 -0
  12. package/dist/cjs/credential-results.js +72 -0
  13. package/dist/cjs/credential-results.js.map +1 -0
  14. package/dist/cjs/data-api-route-generator.d.ts +1 -0
  15. package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
  16. package/dist/cjs/data-api-route-generator.js +13 -5
  17. package/dist/cjs/data-api-route-generator.js.map +1 -1
  18. package/dist/cjs/executor-generator.d.ts.map +1 -1
  19. package/dist/cjs/executor-generator.js +20 -3
  20. package/dist/cjs/executor-generator.js.map +1 -1
  21. package/dist/cjs/nodes/account/account-hash-password.d.ts.map +1 -1
  22. package/dist/cjs/nodes/account/account-hash-password.js +1 -0
  23. package/dist/cjs/nodes/account/account-hash-password.js.map +1 -1
  24. package/dist/cjs/nodes/types.d.ts +6 -0
  25. package/dist/cjs/nodes/types.d.ts.map +1 -1
  26. package/dist/cjs/server-runtime-code.d.ts +8 -0
  27. package/dist/cjs/server-runtime-code.d.ts.map +1 -1
  28. package/dist/cjs/server-runtime-code.js +106 -0
  29. package/dist/cjs/server-runtime-code.js.map +1 -1
  30. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  31. package/dist/esm/api-route-generator.d.ts.map +1 -1
  32. package/dist/esm/api-route-generator.js +6 -2
  33. package/dist/esm/api-route-generator.js.map +1 -1
  34. package/dist/esm/credential-columns-code.d.ts +17 -0
  35. package/dist/esm/credential-columns-code.d.ts.map +1 -0
  36. package/dist/esm/credential-columns-code.js +57 -0
  37. package/dist/esm/credential-columns-code.js.map +1 -0
  38. package/dist/esm/credential-results.d.ts +18 -0
  39. package/dist/esm/credential-results.d.ts.map +1 -0
  40. package/dist/esm/credential-results.js +68 -0
  41. package/dist/esm/credential-results.js.map +1 -0
  42. package/dist/esm/data-api-route-generator.d.ts +1 -0
  43. package/dist/esm/data-api-route-generator.d.ts.map +1 -1
  44. package/dist/esm/data-api-route-generator.js +11 -3
  45. package/dist/esm/data-api-route-generator.js.map +1 -1
  46. package/dist/esm/executor-generator.d.ts.map +1 -1
  47. package/dist/esm/executor-generator.js +20 -3
  48. package/dist/esm/executor-generator.js.map +1 -1
  49. package/dist/esm/nodes/account/account-hash-password.d.ts.map +1 -1
  50. package/dist/esm/nodes/account/account-hash-password.js +1 -0
  51. package/dist/esm/nodes/account/account-hash-password.js.map +1 -1
  52. package/dist/esm/nodes/types.d.ts +6 -0
  53. package/dist/esm/nodes/types.d.ts.map +1 -1
  54. package/dist/esm/server-runtime-code.d.ts +8 -0
  55. package/dist/esm/server-runtime-code.d.ts.map +1 -1
  56. package/dist/esm/server-runtime-code.js +106 -0
  57. package/dist/esm/server-runtime-code.js.map +1 -1
  58. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  59. package/package.json +3 -3
  60. package/src/api-route-generator.ts +6 -2
  61. package/src/credential-columns-code.ts +59 -0
  62. package/src/credential-results.ts +71 -0
  63. package/src/data-api-route-generator.ts +11 -3
  64. package/src/executor-generator.ts +20 -3
  65. package/src/nodes/account/account-hash-password.ts +1 -0
  66. package/src/nodes/types.ts +6 -0
  67. package/src/server-runtime-code.ts +106 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teleporthq/teleport-plugin-next-workflows",
3
- "version": "0.44.6",
3
+ "version": "0.44.8",
4
4
  "description": "A plugin for workflow code generation in Next.js projects.",
5
5
  "author": "teleportHQ",
6
6
  "license": "MIT",
@@ -66,8 +66,8 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "@babel/types": "^7.5.5",
69
- "@teleporthq/teleport-shared": "^0.44.5",
69
+ "@teleporthq/teleport-shared": "^0.44.7",
70
70
  "@teleporthq/teleport-types": "^0.44.0"
71
71
  },
72
- "gitHead": "c763dc9d1d966d36811d04cd422e948a5cd7e841"
72
+ "gitHead": "81bb230e4f5c7b6c339850a8589fa29c78602d66"
73
73
  }
@@ -527,11 +527,15 @@ function __isStreamingAI(node, config) {
527
527
  return config && config.streaming && __streamingAITypes.indexOf(node.type) >= 0;
528
528
  }
529
529
 
530
- // One node's result to the browser — never one kept on the server (see segmentReply).
530
+ // One node's result to the browser — never one kept on the server nor a
531
+ // credential (the closing reply carries that one sealed), and a data node's
532
+ // rows without the users table's credentials (see segmentReply).
533
+ const __credentialResultIds = utils.credentialResultNodeIds(SEGMENT_CONFIG);
531
534
  function __streamNodeResult(res, nodeId, result) {
535
+ if (__credentialResultIds[nodeId]) return;
532
536
  const kept = SEGMENT_CONFIG.nodes.find(function(nd) { return nd.id === nodeId; });
533
537
  if (kept && kept.config && kept.config.keepResultOnServer === true && !utils.isBranchingNodeType(kept.type)) return;
534
- res.write('data: ' + JSON.stringify({ type: 'node-result', nodeId: nodeId, result: result }) + '\\n\\n');
538
+ res.write('data: ' + JSON.stringify({ type: 'node-result', nodeId: nodeId, result: utils.withoutCredentialColumns(kept, result) }) + '\\n\\n');
535
539
  }
536
540
 
537
541
  function __collectSegBranchNodes(startId, excludeId) {
@@ -0,0 +1,59 @@
1
+ import { TableAccess } from '@teleporthq/teleport-shared'
2
+ import { DATA_NODE_TYPES } from './data-api-route-generator'
3
+
4
+ /**
5
+ * Runtime helpers that leave the auth users table's credential columns out of
6
+ * the rows a server segment answers the browser with — the columns
7
+ * `TableAccess.AUTH_CREDENTIAL_COLUMNS` names, the ones every browser read of
8
+ * the data route is already served without. A data node of a server segment
9
+ * reads through the data route as the app's own server code, so its rows come
10
+ * back whole, and the segment's reply (or a streamed node result) is the
11
+ * browser's copy of them.
12
+ *
13
+ * Only the results of the nodes that read rows (`DATA_NODE_TYPES`) are
14
+ * touched: all the credentials of a row of the users table, the unambiguous
15
+ * ones of a raw statement's row, whose table is not known. A row is the
16
+ * result itself (a create or update spreads its row into it), an object it
17
+ * holds, or an element of an array it holds; column values are not walked.
18
+ */
19
+ export const generateCredentialColumnsCode = (): string => `
20
+ var __CREDENTIAL_COLUMNS = ${JSON.stringify(TableAccess.AUTH_CREDENTIAL_COLUMNS)};
21
+ var __UNAMBIGUOUS_CREDENTIAL_COLUMNS = ${JSON.stringify(
22
+ TableAccess.UNAMBIGUOUS_CREDENTIAL_COLUMNS
23
+ )};
24
+ var __CREDENTIAL_TABLES = ${JSON.stringify(TableAccess.AUTH_CREDENTIAL_TABLES)};
25
+ var __ROW_NODE_TYPES = ${JSON.stringify(Array.from(DATA_NODE_TYPES))};
26
+
27
+ function credentialColumnsOf(node) {
28
+ if (!node || __ROW_NODE_TYPES.indexOf(node.type) === -1) return [];
29
+ var tableName = node.config && typeof node.config.tableName === 'string' ? node.config.tableName : '';
30
+ if (!tableName) return __UNAMBIGUOUS_CREDENTIAL_COLUMNS;
31
+ var parts = tableName.trim().toLowerCase().replace(/"/g, '').split('.');
32
+ return __CREDENTIAL_TABLES.indexOf(parts[parts.length - 1]) !== -1 ? __CREDENTIAL_COLUMNS : [];
33
+ }
34
+
35
+ function withoutColumns(row, columns) {
36
+ if (!row || typeof row !== 'object' || Array.isArray(row)) return row;
37
+ var kept = {};
38
+ Object.keys(row).forEach(function(key) {
39
+ if (columns.indexOf(String(key).toLowerCase()) === -1) kept[key] = row[key];
40
+ });
41
+ return kept;
42
+ }
43
+
44
+ function withoutRowsColumns(value, columns) {
45
+ if (Array.isArray(value)) return value.map(function(row) { return withoutColumns(row, columns); });
46
+ return withoutColumns(value, columns);
47
+ }
48
+
49
+ function withoutCredentialColumns(node, result) {
50
+ var columns = credentialColumnsOf(node);
51
+ if (columns.length === 0 || !result || typeof result !== 'object') return result;
52
+ if (Array.isArray(result)) return withoutRowsColumns(result, columns);
53
+ var kept = withoutColumns(result, columns);
54
+ Object.keys(kept).forEach(function(key) {
55
+ if (kept[key] && typeof kept[key] === 'object') kept[key] = withoutRowsColumns(kept[key], columns);
56
+ });
57
+ return kept;
58
+ }
59
+ `
@@ -0,0 +1,71 @@
1
+ import { nodeRegistry } from './nodes'
2
+
3
+ /**
4
+ * The node types whose result is a credential, read off the node registry
5
+ * (`resultIsCredential`): today the password-hashing node. Its result stays on
6
+ * the server. A server segment replies it to the browser only sealed —
7
+ * encrypted with a key derived from the app secret, signed like every other
8
+ * result — and a later server segment opens it again, so a workflow that
9
+ * hashes in one segment and writes in another still works (see the server
10
+ * runtime: `sealResult`, `openSealedResults`).
11
+ */
12
+ export const CREDENTIAL_RESULT_NODE_TYPES: string[] = Object.keys(nodeRegistry).filter(
13
+ (nodeType) => nodeRegistry[nodeType].resultIsCredential === true
14
+ )
15
+
16
+ /**
17
+ * `credentialResultNodeIds(segmentConfig)`: the nodes of a segment whose
18
+ * result is a credential — every node of a credential type, and every map
19
+ * loop whose results are such a node's results (the loop's last body step, as
20
+ * the route runs it; a loop of loops follows its inner loop).
21
+ */
22
+ export const generateCredentialResultsCode = (): string => `
23
+ var __CREDENTIAL_RESULT_NODE_TYPES = ${JSON.stringify(CREDENTIAL_RESULT_NODE_TYPES)};
24
+
25
+ function lastLoopBodyNodeId(loopId, sortedNodes, edges) {
26
+ var start = edges.find(function(e) { return e.source === loopId && e.sourceHandle === 'loop'; });
27
+ if (!start) return null;
28
+ var body = {};
29
+ var queue = [start.target];
30
+ while (queue.length > 0) {
31
+ var current = queue.shift();
32
+ if (body[current] || current === loopId) continue;
33
+ body[current] = true;
34
+ edges.forEach(function(e) {
35
+ if (e.source === current && (e.sourceHandle === 'loop-body-out' || !e.sourceHandle) && e.target !== loopId) {
36
+ queue.push(e.target);
37
+ }
38
+ });
39
+ }
40
+ var last = null;
41
+ sortedNodes.forEach(function(n) { if (body[n.id]) last = n.id; });
42
+ return last;
43
+ }
44
+
45
+ function credentialResultNodeIds(segmentConfig) {
46
+ var nodes = (segmentConfig && segmentConfig.nodes) || [];
47
+ var edges = (segmentConfig && segmentConfig.edges) || [];
48
+ var ids = {};
49
+ nodes.forEach(function(n) {
50
+ if (__CREDENTIAL_RESULT_NODE_TYPES.indexOf(n.type) !== -1) ids[n.id] = true;
51
+ });
52
+ var sortedNodes = nodes.slice().sort(function(a, b) { return (a.stepNumber || 0) - (b.stepNumber || 0); });
53
+ var mapLoops = nodes.filter(function(n) {
54
+ var loopType = n.config && n.config.loopType;
55
+ return n.type === 'general-loop' && !!loopType && loopType !== 'forEach';
56
+ });
57
+ var grew = true;
58
+ while (grew) {
59
+ grew = false;
60
+ mapLoops.forEach(function(loop) {
61
+ if (ids[loop.id]) return;
62
+ var last = lastLoopBodyNodeId(loop.id, sortedNodes, edges);
63
+ if (last && ids[last]) {
64
+ ids[loop.id] = true;
65
+ grew = true;
66
+ }
67
+ });
68
+ }
69
+ return ids;
70
+ }
71
+ `
@@ -15,7 +15,7 @@ import { generateSqlValidatorCode } from './sql-validator'
15
15
  import { generateCommonJsSessionTokenResolverCode } from './session-cookie-resolver'
16
16
  import { ContentTables, TableAccess } from '@teleporthq/teleport-shared'
17
17
 
18
- const DATA_NODE_TYPES = new Set([
18
+ export const DATA_NODE_TYPES: ReadonlySet<string> = new Set([
19
19
  'data-select',
20
20
  'data-create-item',
21
21
  'data-update-item',
@@ -677,6 +677,13 @@ function dropMissingOptionalColumns(tableName, entries, colTypes) {
677
677
  return kept;
678
678
  }
679
679
 
680
+ // The row a create or update answers with, whoever asked, without the auth
681
+ // users table's credentials (the columns a browser read is served without):
682
+ // a server-side caller hands that answer on to the browser as its result.
683
+ function withoutCredentialColumns(row, tableName) {
684
+ return row ? __taWithoutCredentials([row], tableName)[0] : row;
685
+ }
686
+
680
687
  async function handleCreate(client, body) {
681
688
  var tableName = body.tableName;
682
689
  var columnMappings = body.columnMappings || {};
@@ -751,7 +758,7 @@ async function handleCreate(client, body) {
751
758
  var item = result.rows && result.rows[0] ? result.rows[0] : null;
752
759
  var idIdx = columns.indexOf('id');
753
760
  var resolvedId = item ? (item.id || null) : (idIdx >= 0 ? values[idIdx] : null);
754
- return { item: item, id: resolvedId };
761
+ return { item: withoutCredentialColumns(item, tableName), id: resolvedId };
755
762
  }
756
763
 
757
764
  async function handleUpdate(client, body) {
@@ -803,7 +810,8 @@ async function handleUpdate(client, body) {
803
810
  sql += ' RETURNING *';
804
811
 
805
812
  var result = await safeQuery(client, sql, queryParams, 'update');
806
- return { updatedCount: result.rowCount || 0, item: result.rows[0] || null, id: (result.rows[0] && result.rows[0].id) || null };
813
+ var updated = result.rows[0] || null;
814
+ return { updatedCount: result.rowCount || 0, item: withoutCredentialColumns(updated, tableName), id: (updated && updated.id) || null };
807
815
  }
808
816
 
809
817
  async function handleDelete(client, body) {
@@ -4,6 +4,8 @@ import {
4
4
  generateWorkflowLocaleHelpersCode,
5
5
  } from './email-locale'
6
6
  import { generateSignedInDestinationCode } from './auth-return/signed-in-destination-code'
7
+ import { generateCredentialColumnsCode } from './credential-columns-code'
8
+ import { generateCredentialResultsCode } from './credential-results'
7
9
 
8
10
  export interface SharedRuntimeUtilsOptions {
9
11
  /**
@@ -1414,6 +1416,15 @@ var SEGMENT_REPLY_PRIVATE_KEYS = {
1414
1416
  // nodes after it in the same segment and never leaves the server (a statement's
1415
1417
  // rows read only to build what the browser may see, a sender's recipients).
1416
1418
  // Nothing in a later segment can read it.
1419
+ //
1420
+ // A data node's rows are replied without the auth users table's credentials
1421
+ // (see credential-columns-code.ts).
1422
+ //
1423
+ // A node whose result is a credential (a password hash) is left out here too,
1424
+ // whatever its config says: the server runtime replies it sealed, readable by
1425
+ // a later server segment only (see credential-results.ts).
1426
+ ${generateCredentialColumnsCode()}
1427
+ ${generateCredentialResultsCode()}
1417
1428
  // The nodes whose result the client branches on: never kept on the server.
1418
1429
  function isBranchingNodeType(type) {
1419
1430
  return type === 'general-if-statement' || type === 'general-switch';
@@ -1422,10 +1433,10 @@ function isBranchingNodeType(type) {
1422
1433
  function segmentReply(segmentConfig, context, snapshot) {
1423
1434
  var reply = {};
1424
1435
  var own = {};
1425
- var kept = {};
1436
+ var kept = credentialResultNodeIds(segmentConfig);
1426
1437
  var nodes = (segmentConfig && segmentConfig.nodes) || [];
1427
1438
  for (var ni = 0; ni < nodes.length; ni++) {
1428
- own[nodes[ni].id] = true;
1439
+ own[nodes[ni].id] = nodes[ni];
1429
1440
  if (nodes[ni].config && nodes[ni].config.keepResultOnServer === true && !isBranchingNodeType(nodes[ni].type)) {
1430
1441
  kept[nodes[ni].id] = true;
1431
1442
  }
@@ -1436,7 +1447,11 @@ function segmentReply(segmentConfig, context, snapshot) {
1436
1447
  if (SEGMENT_REPLY_PRIVATE_KEYS[key] || kept[key]) continue;
1437
1448
  var value = context[key];
1438
1449
  if (value === undefined) continue;
1439
- if (own[key] || !snapshot || !Object.prototype.hasOwnProperty.call(snapshot, key)) {
1450
+ if (own[key]) {
1451
+ reply[key] = withoutCredentialColumns(own[key], value);
1452
+ continue;
1453
+ }
1454
+ if (!snapshot || !Object.prototype.hasOwnProperty.call(snapshot, key)) {
1440
1455
  reply[key] = value;
1441
1456
  continue;
1442
1457
  }
@@ -1504,6 +1519,8 @@ module.exports = {
1504
1519
  loopIterationContext,
1505
1520
  snapshotIncomingContext,
1506
1521
  segmentReply,
1522
+ withoutCredentialColumns,
1523
+ credentialResultNodeIds,
1507
1524
  evaluateCondition,
1508
1525
  evaluateSingleComparison,
1509
1526
  normalizeComparisonOperator,
@@ -47,6 +47,7 @@ async function account_hash_password(config: any, context: Record<string, unknow
47
47
  export const accountHashPassword: NodeHandlerGenerator = {
48
48
  nodeType: 'account-hash-password',
49
49
  executionEnv: 'server',
50
+ resultIsCredential: true,
50
51
  dependencies: {
51
52
  bcryptjs: '^2.4.3',
52
53
  },
@@ -114,6 +114,12 @@ export interface NodeHandlerGenerator {
114
114
  nodeType: string
115
115
  executionEnv: 'client' | 'server' | 'universal'
116
116
  isTerminal?: boolean
117
+ /**
118
+ * The node's result is a credential (a password hash). A server segment
119
+ * never replies it as it is: the browser receives it sealed, and only the
120
+ * app's own later server segments can open it (see credential-results.ts).
121
+ */
122
+ resultIsCredential?: boolean
117
123
  dependencies?: Record<string, string>
118
124
  generateHandler(): string
119
125
  generateServerHandler?(): string
@@ -27,6 +27,14 @@
27
27
  * that needs a trustworthy figure (a price, an owner) reads it from the
28
28
  * database itself.
29
29
  *
30
+ * A result that is a credential (a password hash, see credential-results.ts)
31
+ * makes the same trip SEALED: `segmentReply` replies it encrypted with a key
32
+ * derived from NEXTAUTH_SECRET (AES-256-GCM) and signs the sealed form, and
33
+ * `claimSegmentContext` opens every sealed value a request hands it (a
34
+ * relayed result once its signature holds). The browser relays it without
35
+ * ever reading it, and a later segment's reply hands back the sealed form,
36
+ * never the opened one.
37
+ *
30
38
  * The facts about the graph each route needs are baked into its
31
39
  * SEGMENT_CONFIG at generation time (see segment-trust.ts).
32
40
  */
@@ -51,6 +59,59 @@ function mac(text) {
51
59
  return crypto.createHmac('sha256', stepKey()).update(text).digest('hex');
52
60
  }
53
61
 
62
+ var __sealKeyCache = { secret: null, key: null };
63
+
64
+ function sealKey() {
65
+ var secret = process.env.NEXTAUTH_SECRET || '';
66
+ if (__sealKeyCache.secret !== secret) {
67
+ __sealKeyCache = {
68
+ secret: secret,
69
+ key: crypto.createHmac('sha256', secret).update('teleport-workflow-seal:v1').digest()
70
+ };
71
+ }
72
+ return __sealKeyCache.key;
73
+ }
74
+
75
+ var SEAL_IV_BYTES = 12;
76
+ var SEAL_TAG_BYTES = 16;
77
+
78
+ var SEAL_AAD = Buffer.from('teleport-workflow-sealed-result:v1', 'utf8');
79
+
80
+ // The sealed form of a credential result; undefined (the result is then not
81
+ // replied at all) without an app secret, whose key anyone could derive.
82
+ function sealResult(value) {
83
+ if (!process.env.NEXTAUTH_SECRET) return undefined;
84
+ var serialized;
85
+ try { serialized = JSON.stringify(value); } catch (e) { return undefined; }
86
+ if (serialized === undefined) return undefined;
87
+ var iv = crypto.randomBytes(SEAL_IV_BYTES);
88
+ var cipher = crypto.createCipheriv('aes-256-gcm', sealKey(), iv);
89
+ cipher.setAAD(SEAL_AAD);
90
+ var data = Buffer.concat([cipher.update(serialized, 'utf8'), cipher.final()]);
91
+ return { __sealed: Buffer.concat([iv, cipher.getAuthTag(), data]).toString('base64') };
92
+ }
93
+
94
+ function isSealedResult(value) {
95
+ return !!value && typeof value === 'object' && !Array.isArray(value) &&
96
+ typeof value.__sealed === 'string' && Object.keys(value).length === 1;
97
+ }
98
+
99
+ // { value } when the sealed form opens under this app's key, else null.
100
+ function openSealedResult(sealed) {
101
+ if (!process.env.NEXTAUTH_SECRET) return null;
102
+ try {
103
+ var raw = Buffer.from(sealed.__sealed, 'base64');
104
+ if (raw.length <= SEAL_IV_BYTES + SEAL_TAG_BYTES) return null;
105
+ var decipher = crypto.createDecipheriv('aes-256-gcm', sealKey(), raw.subarray(0, SEAL_IV_BYTES));
106
+ decipher.setAAD(SEAL_AAD);
107
+ decipher.setAuthTag(raw.subarray(SEAL_IV_BYTES, SEAL_IV_BYTES + SEAL_TAG_BYTES));
108
+ var data = Buffer.concat([decipher.update(raw.subarray(SEAL_IV_BYTES + SEAL_TAG_BYTES)), decipher.final()]);
109
+ return { value: JSON.parse(data.toString('utf8')) };
110
+ } catch (e) {
111
+ return null;
112
+ }
113
+ }
114
+
54
115
  // Without an app secret every key here would derive from '' — a value anyone
55
116
  // can compute — so nothing verifies: relayed server results are dropped and no
56
117
  // request passes as an internal call. (The generator gives every app with
@@ -206,10 +267,48 @@ function resetPreviousResult(segmentConfig, context) {
206
267
  // merge will.
207
268
  var __incomingOrders = new WeakMap();
208
269
 
270
+ // The sealed results this segment opened, so its reply hands the caller back
271
+ // the sealed form it relayed (see resealRelayedResults).
272
+ var __openedResults = new WeakMap();
273
+
274
+ // Opens every sealed value the request hands this segment, so its nodes read
275
+ // the credential itself: a relayed server result (whose signature
276
+ // dropUnsignedResults checked first), or a value the browser holds from one
277
+ // (a custom node's output, the previous result). Only this app's server
278
+ // seals, so what opens is what it sealed.
279
+ function openSealedResults(context) {
280
+ var opened = [];
281
+ Object.keys(context).forEach(function(key) {
282
+ if (isControlKey(key) && key !== '__previousNodeResult') return;
283
+ if (!isSealedResult(context[key])) return;
284
+ var result = openSealedResult(context[key]);
285
+ if (!result) return;
286
+ opened.push({ sealed: context[key], value: result.value });
287
+ context[key] = result.value;
288
+ });
289
+ __openedResults.set(context, opened);
290
+ }
291
+
292
+ // Every reply entry that is an opened value (the value itself, or the previous
293
+ // result taken from it) goes back sealed.
294
+ function resealRelayedResults(context, reply) {
295
+ var opened = __openedResults.get(context) || [];
296
+ if (opened.length === 0) return;
297
+ Object.keys(reply).forEach(function(key) {
298
+ for (var i = 0; i < opened.length; i++) {
299
+ if (reply[key] === opened[i].value) {
300
+ reply[key] = opened[i].sealed;
301
+ return;
302
+ }
303
+ }
304
+ });
305
+ }
306
+
209
307
  function claimSegmentContext(segmentConfig, context) {
210
308
  var orderKeys = readSignedOrder(context);
211
309
  if (orderKeys) restoreOrder(context, orderKeys);
212
310
  dropUnsignedResults(segmentConfig, context, orderKeys);
311
+ openSealedResults(context);
213
312
  stripForeignControlKeys(segmentConfig, context);
214
313
  __incomingOrders.set(context, Object.keys(context).filter(function(k) { return !isControlKey(k); }));
215
314
  utils.claimSegmentContext(segmentConfig, context);
@@ -219,6 +318,13 @@ function claimSegmentContext(segmentConfig, context) {
219
318
 
220
319
  function segmentReply(segmentConfig, context, snapshot) {
221
320
  var reply = utils.segmentReply(segmentConfig, context, snapshot);
321
+ resealRelayedResults(context, reply);
322
+ var credentialIds = utils.credentialResultNodeIds(segmentConfig);
323
+ Object.keys(credentialIds).forEach(function(id) {
324
+ if (context[id] === undefined) return;
325
+ var sealed = sealResult(context[id]);
326
+ if (sealed) reply[id] = sealed;
327
+ });
222
328
  var sigs = {};
223
329
  var incoming = context.__sig && typeof context.__sig === 'object' && !Array.isArray(context.__sig) ? context.__sig : {};
224
330
  Object.keys(incoming).forEach(function(id) {