@prisma-next/extension-cipherstash 0.0.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/README.md +153 -0
- package/dist/call-classes-CSvD7w8U.mjs +206 -0
- package/dist/call-classes-CSvD7w8U.mjs.map +1 -0
- package/dist/column-types.d.mts +33 -0
- package/dist/column-types.d.mts.map +1 -0
- package/dist/column-types.mjs +42 -0
- package/dist/column-types.mjs.map +1 -0
- package/dist/constants-BDxL9Pe3.d.mts +22 -0
- package/dist/constants-BDxL9Pe3.d.mts.map +1 -0
- package/dist/constants-B_2TNvUi.mjs +46 -0
- package/dist/constants-B_2TNvUi.mjs.map +1 -0
- package/dist/control.d.mts +7 -0
- package/dist/control.d.mts.map +1 -0
- package/dist/control.mjs +430 -0
- package/dist/control.mjs.map +1 -0
- package/dist/descriptor-meta-BgQfZTAF.mjs +129 -0
- package/dist/descriptor-meta-BgQfZTAF.mjs.map +1 -0
- package/dist/envelope-P9BxfJNr.mjs +271 -0
- package/dist/envelope-P9BxfJNr.mjs.map +1 -0
- package/dist/middleware.d.mts +13 -0
- package/dist/middleware.d.mts.map +1 -0
- package/dist/middleware.mjs +129 -0
- package/dist/middleware.mjs.map +1 -0
- package/dist/migration.d.mts +141 -0
- package/dist/migration.d.mts.map +1 -0
- package/dist/migration.mjs +2 -0
- package/dist/operation-types.d.mts +49 -0
- package/dist/operation-types.d.mts.map +1 -0
- package/dist/operation-types.mjs +1 -0
- package/dist/pack.d.mts +86 -0
- package/dist/pack.d.mts.map +1 -0
- package/dist/pack.mjs +2 -0
- package/dist/runtime.d.mts +207 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +429 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/sdk-D5FTGyzp.d.mts +67 -0
- package/dist/sdk-D5FTGyzp.d.mts.map +1 -0
- package/package.json +69 -0
- package/src/contract/authoring.ts +62 -0
- package/src/contract/contract.d.ts +149 -0
- package/src/contract/contract.json +104 -0
- package/src/contract/contract.prisma +46 -0
- package/src/execution/abort.ts +143 -0
- package/src/execution/codec-runtime.ts +209 -0
- package/src/execution/decrypt-all.ts +217 -0
- package/src/execution/envelope.ts +263 -0
- package/src/execution/operators.ts +211 -0
- package/src/execution/parameterized.ts +71 -0
- package/src/execution/routing.ts +93 -0
- package/src/execution/sdk.ts +68 -0
- package/src/exports/column-types.ts +62 -0
- package/src/exports/contract-space-typing.ts +86 -0
- package/src/exports/control.ts +120 -0
- package/src/exports/middleware.ts +24 -0
- package/src/exports/migration.ts +43 -0
- package/src/exports/operation-types.ts +16 -0
- package/src/exports/pack.ts +13 -0
- package/src/exports/runtime.ts +110 -0
- package/src/extension-metadata/codec-metadata.ts +81 -0
- package/src/extension-metadata/constants.ts +70 -0
- package/src/extension-metadata/descriptor-meta.ts +76 -0
- package/src/middleware/bulk-encrypt.ts +192 -0
- package/src/migration/call-classes.ts +350 -0
- package/src/migration/cipherstash-codec.ts +157 -0
- package/src/migration/eql-bundle.ts +29 -0
- package/src/migration/eql-install.generated.ts +5751 -0
- package/src/types/operation-types.ts +81 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authoring contributions for the cipherstash extension.
|
|
3
|
+
*
|
|
4
|
+
* Registers `cipherstash.EncryptedString({ equality?, freeTextSearch? })`
|
|
5
|
+
* as a namespaced PSL type constructor. The same descriptor lowers a
|
|
6
|
+
* PSL field-type expression like `cipherstash.EncryptedString({ equality:
|
|
7
|
+
* true })` and a TS factory call like `encryptedString({ equality: true })`
|
|
8
|
+
* (see `../exports/column-types`) to an identical `ColumnTypeDescriptor`
|
|
9
|
+
* so PSL- and TS-authored contracts emit byte-identical `contract.json`.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors `packages/3-extensions/pgvector/src/contract/authoring.ts`. The
|
|
12
|
+
* cipherstash variant differs in three respects:
|
|
13
|
+
* (a) `cipherstash` is the namespace,
|
|
14
|
+
* (b) the constructor takes a single OPTIONAL object argument with two
|
|
15
|
+
* optional booleans (so `cipherstash.EncryptedString()`,
|
|
16
|
+
* `cipherstash.EncryptedString({})`, and the fully-spelled
|
|
17
|
+
* `cipherstash.EncryptedString({ equality: true, freeTextSearch: true })`
|
|
18
|
+
* all parse), and
|
|
19
|
+
* (c) both flags default to `true` — searchable encryption is the
|
|
20
|
+
* legitimate default for an extension whose entire reason for
|
|
21
|
+
* existing is to make encrypted columns queryable. Users who want
|
|
22
|
+
* storage-only encryption opt out explicitly:
|
|
23
|
+
* `cipherstash.EncryptedString({ equality: false, freeTextSearch: false })`.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { AuthoringTypeNamespace } from '@prisma-next/framework-components/authoring';
|
|
27
|
+
import {
|
|
28
|
+
CIPHERSTASH_STRING_CODEC_ID,
|
|
29
|
+
EQL_V2_ENCRYPTED_TYPE,
|
|
30
|
+
} from '../extension-metadata/constants';
|
|
31
|
+
|
|
32
|
+
export const cipherstashAuthoringTypes = {
|
|
33
|
+
cipherstash: {
|
|
34
|
+
EncryptedString: {
|
|
35
|
+
kind: 'typeConstructor',
|
|
36
|
+
args: [
|
|
37
|
+
{
|
|
38
|
+
kind: 'object',
|
|
39
|
+
name: 'options',
|
|
40
|
+
optional: true,
|
|
41
|
+
properties: {
|
|
42
|
+
equality: { kind: 'boolean', optional: true },
|
|
43
|
+
freeTextSearch: { kind: 'boolean', optional: true },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
output: {
|
|
48
|
+
codecId: CIPHERSTASH_STRING_CODEC_ID,
|
|
49
|
+
nativeType: EQL_V2_ENCRYPTED_TYPE,
|
|
50
|
+
typeParams: {
|
|
51
|
+
equality: { kind: 'arg', index: 0, path: ['equality'], default: true },
|
|
52
|
+
freeTextSearch: {
|
|
53
|
+
kind: 'arg',
|
|
54
|
+
index: 0,
|
|
55
|
+
path: ['freeTextSearch'],
|
|
56
|
+
default: true,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
} as const satisfies AuthoringTypeNamespace;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// ⚠️ GENERATED FILE - DO NOT EDIT
|
|
2
|
+
// This file is automatically generated by 'prisma-next contract emit'.
|
|
3
|
+
// To regenerate, run: prisma-next contract emit
|
|
4
|
+
import type { CodecTypes as PgTypes } from '@prisma-next/target-postgres/codec-types';
|
|
5
|
+
import type { JsonValue } from '@prisma-next/target-postgres/codec-types';
|
|
6
|
+
import type { Char } from '@prisma-next/target-postgres/codec-types';
|
|
7
|
+
import type { Varchar } from '@prisma-next/target-postgres/codec-types';
|
|
8
|
+
import type { Numeric } from '@prisma-next/target-postgres/codec-types';
|
|
9
|
+
import type { Bit } from '@prisma-next/target-postgres/codec-types';
|
|
10
|
+
import type { VarBit } from '@prisma-next/target-postgres/codec-types';
|
|
11
|
+
import type { Timestamp } from '@prisma-next/target-postgres/codec-types';
|
|
12
|
+
import type { Timestamptz } from '@prisma-next/target-postgres/codec-types';
|
|
13
|
+
import type { Time } from '@prisma-next/target-postgres/codec-types';
|
|
14
|
+
import type { Timetz } from '@prisma-next/target-postgres/codec-types';
|
|
15
|
+
import type { Interval } from '@prisma-next/target-postgres/codec-types';
|
|
16
|
+
import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types';
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
ContractWithTypeMaps,
|
|
20
|
+
TypeMaps as TypeMapsType,
|
|
21
|
+
} from '@prisma-next/sql-contract/types';
|
|
22
|
+
import type {
|
|
23
|
+
Contract as ContractType,
|
|
24
|
+
ExecutionHashBase,
|
|
25
|
+
ProfileHashBase,
|
|
26
|
+
StorageHashBase,
|
|
27
|
+
} from '@prisma-next/contract/types';
|
|
28
|
+
|
|
29
|
+
export type StorageHash =
|
|
30
|
+
StorageHashBase<'sha256:efa685171bebbb8f078f08d12be3578bb5d96b71669dccc6cc9e4be96af8cdb4'>;
|
|
31
|
+
export type ExecutionHash = ExecutionHashBase<string>;
|
|
32
|
+
export type ProfileHash =
|
|
33
|
+
ProfileHashBase<'sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e'>;
|
|
34
|
+
|
|
35
|
+
export type CodecTypes = PgTypes;
|
|
36
|
+
export type OperationTypes = Record<string, never>;
|
|
37
|
+
export type LaneCodecTypes = CodecTypes;
|
|
38
|
+
export type QueryOperationTypes = PgAdapterQueryOps<CodecTypes>;
|
|
39
|
+
type DefaultLiteralValue<CodecId extends string, _Encoded> = CodecId extends keyof CodecTypes
|
|
40
|
+
? CodecTypes[CodecId]['output']
|
|
41
|
+
: _Encoded;
|
|
42
|
+
|
|
43
|
+
export type FieldOutputTypes = {
|
|
44
|
+
readonly EqlV2Configuration: {
|
|
45
|
+
readonly id: CodecTypes['pg/text@1']['output'];
|
|
46
|
+
readonly state: CodecTypes['pg/text@1']['output'];
|
|
47
|
+
readonly data: CodecTypes['pg/jsonb@1']['output'];
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
export type FieldInputTypes = {
|
|
51
|
+
readonly EqlV2Configuration: {
|
|
52
|
+
readonly id: CodecTypes['pg/text@1']['input'];
|
|
53
|
+
readonly state: CodecTypes['pg/text@1']['input'];
|
|
54
|
+
readonly data: CodecTypes['pg/jsonb@1']['input'];
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
export type TypeMaps = TypeMapsType<
|
|
58
|
+
CodecTypes,
|
|
59
|
+
OperationTypes,
|
|
60
|
+
QueryOperationTypes,
|
|
61
|
+
FieldOutputTypes,
|
|
62
|
+
FieldInputTypes
|
|
63
|
+
>;
|
|
64
|
+
|
|
65
|
+
type ContractBase = ContractType<
|
|
66
|
+
{
|
|
67
|
+
readonly tables: {
|
|
68
|
+
readonly eql_v2_configuration: {
|
|
69
|
+
columns: {
|
|
70
|
+
readonly id: {
|
|
71
|
+
readonly nativeType: 'text';
|
|
72
|
+
readonly codecId: 'pg/text@1';
|
|
73
|
+
readonly nullable: false;
|
|
74
|
+
};
|
|
75
|
+
readonly state: {
|
|
76
|
+
readonly nativeType: 'text';
|
|
77
|
+
readonly codecId: 'pg/text@1';
|
|
78
|
+
readonly nullable: false;
|
|
79
|
+
};
|
|
80
|
+
readonly data: {
|
|
81
|
+
readonly nativeType: 'jsonb';
|
|
82
|
+
readonly codecId: 'pg/jsonb@1';
|
|
83
|
+
readonly nullable: false;
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
primaryKey: { readonly columns: readonly ['id'] };
|
|
87
|
+
uniques: readonly [];
|
|
88
|
+
indexes: readonly [];
|
|
89
|
+
foreignKeys: readonly [];
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
readonly types: Record<string, never>;
|
|
93
|
+
readonly storageHash: StorageHash;
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
readonly EqlV2Configuration: {
|
|
97
|
+
readonly fields: {
|
|
98
|
+
readonly id: {
|
|
99
|
+
readonly nullable: false;
|
|
100
|
+
readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' };
|
|
101
|
+
};
|
|
102
|
+
readonly state: {
|
|
103
|
+
readonly nullable: false;
|
|
104
|
+
readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' };
|
|
105
|
+
};
|
|
106
|
+
readonly data: {
|
|
107
|
+
readonly nullable: false;
|
|
108
|
+
readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/jsonb@1' };
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
readonly relations: Record<string, never>;
|
|
112
|
+
readonly storage: {
|
|
113
|
+
readonly table: 'eql_v2_configuration';
|
|
114
|
+
readonly fields: {
|
|
115
|
+
readonly id: { readonly column: 'id' };
|
|
116
|
+
readonly state: { readonly column: 'state' };
|
|
117
|
+
readonly data: { readonly column: 'data' };
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
> & {
|
|
123
|
+
readonly target: 'postgres';
|
|
124
|
+
readonly targetFamily: 'sql';
|
|
125
|
+
readonly roots: { readonly eql_v2_configuration: 'EqlV2Configuration' };
|
|
126
|
+
readonly capabilities: {
|
|
127
|
+
readonly postgres: {
|
|
128
|
+
readonly jsonAgg: true;
|
|
129
|
+
readonly lateral: true;
|
|
130
|
+
readonly limit: true;
|
|
131
|
+
readonly orderBy: true;
|
|
132
|
+
readonly returning: true;
|
|
133
|
+
};
|
|
134
|
+
readonly sql: {
|
|
135
|
+
readonly defaultInInsert: true;
|
|
136
|
+
readonly enums: true;
|
|
137
|
+
readonly returning: true;
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
readonly extensionPacks: {};
|
|
141
|
+
readonly meta: {};
|
|
142
|
+
|
|
143
|
+
readonly profileHash: ProfileHash;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export type Contract = ContractWithTypeMaps<ContractBase, TypeMaps>;
|
|
147
|
+
|
|
148
|
+
export type Tables = Contract['storage']['tables'];
|
|
149
|
+
export type Models = Contract['models'];
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1",
|
|
3
|
+
"targetFamily": "sql",
|
|
4
|
+
"target": "postgres",
|
|
5
|
+
"profileHash": "sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e",
|
|
6
|
+
"roots": {
|
|
7
|
+
"eql_v2_configuration": "EqlV2Configuration"
|
|
8
|
+
},
|
|
9
|
+
"models": {
|
|
10
|
+
"EqlV2Configuration": {
|
|
11
|
+
"fields": {
|
|
12
|
+
"data": {
|
|
13
|
+
"nullable": false,
|
|
14
|
+
"type": {
|
|
15
|
+
"codecId": "pg/jsonb@1",
|
|
16
|
+
"kind": "scalar"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"id": {
|
|
20
|
+
"nullable": false,
|
|
21
|
+
"type": {
|
|
22
|
+
"codecId": "pg/text@1",
|
|
23
|
+
"kind": "scalar"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"state": {
|
|
27
|
+
"nullable": false,
|
|
28
|
+
"type": {
|
|
29
|
+
"codecId": "pg/text@1",
|
|
30
|
+
"kind": "scalar"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"relations": {},
|
|
35
|
+
"storage": {
|
|
36
|
+
"fields": {
|
|
37
|
+
"data": {
|
|
38
|
+
"column": "data"
|
|
39
|
+
},
|
|
40
|
+
"id": {
|
|
41
|
+
"column": "id"
|
|
42
|
+
},
|
|
43
|
+
"state": {
|
|
44
|
+
"column": "state"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"table": "eql_v2_configuration"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"storage": {
|
|
52
|
+
"storageHash": "sha256:efa685171bebbb8f078f08d12be3578bb5d96b71669dccc6cc9e4be96af8cdb4",
|
|
53
|
+
"tables": {
|
|
54
|
+
"eql_v2_configuration": {
|
|
55
|
+
"columns": {
|
|
56
|
+
"data": {
|
|
57
|
+
"codecId": "pg/jsonb@1",
|
|
58
|
+
"nativeType": "jsonb",
|
|
59
|
+
"nullable": false
|
|
60
|
+
},
|
|
61
|
+
"id": {
|
|
62
|
+
"codecId": "pg/text@1",
|
|
63
|
+
"nativeType": "text",
|
|
64
|
+
"nullable": false
|
|
65
|
+
},
|
|
66
|
+
"state": {
|
|
67
|
+
"codecId": "pg/text@1",
|
|
68
|
+
"nativeType": "text",
|
|
69
|
+
"nullable": false
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"foreignKeys": [],
|
|
73
|
+
"indexes": [],
|
|
74
|
+
"primaryKey": {
|
|
75
|
+
"columns": [
|
|
76
|
+
"id"
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
"uniques": []
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"capabilities": {
|
|
84
|
+
"postgres": {
|
|
85
|
+
"jsonAgg": true,
|
|
86
|
+
"lateral": true,
|
|
87
|
+
"limit": true,
|
|
88
|
+
"orderBy": true,
|
|
89
|
+
"returning": true
|
|
90
|
+
},
|
|
91
|
+
"sql": {
|
|
92
|
+
"defaultInInsert": true,
|
|
93
|
+
"enums": true,
|
|
94
|
+
"returning": true
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"extensionPacks": {},
|
|
98
|
+
"meta": {},
|
|
99
|
+
"_generated": {
|
|
100
|
+
"warning": "⚠️ GENERATED FILE - DO NOT EDIT",
|
|
101
|
+
"message": "This file is automatically generated by \"prisma-next contract emit\".",
|
|
102
|
+
"regenerate": "To regenerate, run: prisma-next contract emit"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// PSL contract source for the `extension-cipherstash` package.
|
|
2
|
+
//
|
|
3
|
+
// Authored against the on-disk-in-package convention. The same emit
|
|
4
|
+
// pipeline application authors use is applied here:
|
|
5
|
+
//
|
|
6
|
+
// `prisma-next contract emit` → `<package>/src/contract/contract.{json,d.ts}`
|
|
7
|
+
// `prisma-next migration plan` → `<package>/migrations/cipherstash/<dirName>/`
|
|
8
|
+
//
|
|
9
|
+
// The descriptor at `src/exports/control.ts` then wires the emitted JSON
|
|
10
|
+
// artefacts via JSON-import declarations.
|
|
11
|
+
//
|
|
12
|
+
// ## IR coverage and explicit deferral
|
|
13
|
+
//
|
|
14
|
+
// CipherStash should declare four kinds of typed objects in its
|
|
15
|
+
// contract IR: tables, enums, composite types, and domains. Of these,
|
|
16
|
+
// today's `SqlStorage` IR (`@prisma-next/sql-contract/types`) only
|
|
17
|
+
// models tables and parameterised type instances (a fit for things
|
|
18
|
+
// like pgvector's `vector(N)`, but not yet codec-less composite types,
|
|
19
|
+
// standalone enums, or domains).
|
|
20
|
+
//
|
|
21
|
+
// The contract therefore declares the only IR-representable object
|
|
22
|
+
// today (the `eql_v2_configuration` table) using portable scalar
|
|
23
|
+
// types (`String` / `Json`). The actual database state — the `eql_v2`
|
|
24
|
+
// schema, the typed `eql_v2_configuration_state` enum, the
|
|
25
|
+
// `eql_v2_encrypted` composite, the `eql_v2.bloom_filter` /
|
|
26
|
+
// `hmac_256` / `blake3` domains, and the various `ore_*` composites —
|
|
27
|
+
// is created by the `installEqlBundle` migration op (which carries
|
|
28
|
+
// the vendored bundle SQL byte-for-byte; see
|
|
29
|
+
// `./src/migration/eql-bundle.ts`). The structural
|
|
30
|
+
// `cipherstash:create-*-v1` no-op ops register the invariantIds the
|
|
31
|
+
// verifier needs so its `applied_invariants` gate passes.
|
|
32
|
+
//
|
|
33
|
+
// Once the IR vocabulary expands to first-class composite types,
|
|
34
|
+
// standalone enums, and domains, those typed objects shift up into
|
|
35
|
+
// `storage.types` and the structural ops gain real verification work
|
|
36
|
+
// (precheck SQL probing `pg_type` / `information_schema`).
|
|
37
|
+
//
|
|
38
|
+
// @see docs/architecture docs/adrs/ADR 211 - Contract spaces.md
|
|
39
|
+
|
|
40
|
+
model EqlV2Configuration {
|
|
41
|
+
id String @id
|
|
42
|
+
state String
|
|
43
|
+
data Json
|
|
44
|
+
|
|
45
|
+
@@map("eql_v2_configuration")
|
|
46
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cipherstash-internal `RUNTIME.ABORTED` phase wrapping.
|
|
3
|
+
*
|
|
4
|
+
* The framework`s `runtimeAborted(phase)` (`@prisma-next/framework-
|
|
5
|
+
* components/runtime`) constructs the canonical `RUNTIME.ABORTED`
|
|
6
|
+
* envelope (`code === 'RUNTIME.ABORTED'`, `category === 'RUNTIME'`,
|
|
7
|
+
* `details.phase`, `cause`) but its `phase` parameter is typed as
|
|
8
|
+
* the framework`s closed `RuntimeAbortedPhase` union — `encode`,
|
|
9
|
+
* `decode`, `stream`, `beforeExecute`, `afterExecute`, `onRow`. Those
|
|
10
|
+
* tags describe phases of `runtime.execute()` itself (see ADR 207`s
|
|
11
|
+
* "Where the runtime observes abort" table); cipherstash`s async
|
|
12
|
+
* observation points sit one layer outside the framework runtime:
|
|
13
|
+
*
|
|
14
|
+
* - `bulk-encrypt` — the bulk-encrypt middleware`s SDK round-trip
|
|
15
|
+
* inside `beforeExecute`. Conceptually a sub-phase of the
|
|
16
|
+
* framework`s `beforeExecute`, but tag-wise distinct so callers
|
|
17
|
+
* can attribute the abort to the cipherstash SDK call rather
|
|
18
|
+
* than to a generic middleware step.
|
|
19
|
+
* - `decrypt` — the single-cell `EncryptedString#decrypt()`
|
|
20
|
+
* SDK call, invoked by the application after the framework
|
|
21
|
+
* returns the row. Not inside any framework phase.
|
|
22
|
+
* - `decrypt-all` — the `decryptAll` walker`s `bulkDecrypt` calls,
|
|
23
|
+
* invoked by the application after the framework returns the
|
|
24
|
+
* row set. Not inside any framework phase.
|
|
25
|
+
*
|
|
26
|
+
* Rather than widen the framework union (which would conflate
|
|
27
|
+
* extension-specific tags with the framework`s own attribution
|
|
28
|
+
* sites), this module reuses the framework`s `runtimeError(...)`
|
|
29
|
+
* envelope builder directly — the *envelope shape* (the
|
|
30
|
+
* `RuntimeErrorEnvelope` interface, the `code` slot, the `category`
|
|
31
|
+
* slot, the `details.phase` slot, the `cause` field) is unchanged;
|
|
32
|
+
* only the set of legal `phase` string values grows. ADR 027`s
|
|
33
|
+
* envelope contract is preserved bit-for-bit.
|
|
34
|
+
*
|
|
35
|
+
* The `raceCipherstashAbort` helper mirrors framework
|
|
36
|
+
* `raceAgainstAbort` so cipherstash`s SDK-call sites get the same
|
|
37
|
+
* "return promptly even when the SDK ignores the signal" behaviour
|
|
38
|
+
* (the cooperative-cancellation model from ADR 207). Identity-
|
|
39
|
+
* checked sentinel rejection distinguishes abort-source from a
|
|
40
|
+
* codec-thrown envelope, matching the framework`s pattern. We
|
|
41
|
+
* duplicate the logic (rather than passing a cast tag to the
|
|
42
|
+
* framework helper) to keep the cipherstash `phase` strings
|
|
43
|
+
* cipherstash-internal — no widening of the framework union.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import type { RuntimeErrorEnvelope } from '@prisma-next/framework-components/runtime';
|
|
47
|
+
import { RUNTIME_ABORTED, runtimeError } from '@prisma-next/framework-components/runtime';
|
|
48
|
+
|
|
49
|
+
/** Discriminator placed in `details.phase` of cipherstash-issued aborts. */
|
|
50
|
+
export type CipherstashAbortPhase = 'bulk-encrypt' | 'decrypt' | 'decrypt-all';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Construct a `RUNTIME.ABORTED` envelope tagged with a cipherstash
|
|
54
|
+
* phase. Reuses the framework`s `runtimeError(RUNTIME_ABORTED, ...)`
|
|
55
|
+
* envelope builder so the structural shape (`code`, `category`,
|
|
56
|
+
* `severity`, `message`, `details.phase`, `cause`) matches everything
|
|
57
|
+
* else the framework emits. Only the `phase` string set is
|
|
58
|
+
* cipherstash-specific.
|
|
59
|
+
*/
|
|
60
|
+
export function cipherstashAborted(
|
|
61
|
+
phase: CipherstashAbortPhase,
|
|
62
|
+
cause?: unknown,
|
|
63
|
+
): RuntimeErrorEnvelope {
|
|
64
|
+
const envelope = runtimeError(RUNTIME_ABORTED, `Operation aborted during ${phase}`, { phase });
|
|
65
|
+
return Object.assign(envelope, { cause });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Pre-check helper: throw a cipherstash-tagged `RUNTIME.ABORTED`
|
|
70
|
+
* envelope if the supplied signal is already aborted at the call
|
|
71
|
+
* site. Mirrors framework `checkAborted` (which is typed against the
|
|
72
|
+
* framework`s phase union) — used to short-circuit the bulk-encrypt
|
|
73
|
+
* middleware`s pre-flight, the single-cell `decrypt()` pre-flight,
|
|
74
|
+
* and the `decryptAll` walker`s pre-flight before any SDK round-trip
|
|
75
|
+
* is scheduled.
|
|
76
|
+
*/
|
|
77
|
+
export function checkCipherstashAborted(
|
|
78
|
+
signal: AbortSignal | undefined,
|
|
79
|
+
phase: CipherstashAbortPhase,
|
|
80
|
+
): void {
|
|
81
|
+
if (signal?.aborted) {
|
|
82
|
+
throw cipherstashAborted(phase, signal.reason);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Race a cipherstash SDK promise against the supplied `AbortSignal`
|
|
88
|
+
* so the awaiting caller is rejected promptly with a
|
|
89
|
+
* `RUNTIME.ABORTED` envelope as soon as the signal aborts — even
|
|
90
|
+
* when the SDK body itself ignores the signal. Cooperative
|
|
91
|
+
* cancellation: in-flight SDK calls that ignore the signal continue
|
|
92
|
+
* running in the background and complete; the abort-attributed
|
|
93
|
+
* rejection is what the cipherstash caller sees (the SDK`s eventual
|
|
94
|
+
* resolution is silently abandoned per ADR 207`s "cooperative
|
|
95
|
+
* cancellation, not termination" contract).
|
|
96
|
+
*
|
|
97
|
+
* Mirrors framework `raceAgainstAbort` line-for-line aside from the
|
|
98
|
+
* cipherstash-typed phase parameter and the cipherstash-tagged
|
|
99
|
+
* envelope construction. The sentinel-identity attribution is
|
|
100
|
+
* load-bearing for the same reason ADR 207 spells out: a codec /
|
|
101
|
+
* SDK that itself throws a `RUNTIME.ENCODE_FAILED` /
|
|
102
|
+
* `RUNTIME.DECODE_FAILED` (or any other named envelope) must pass
|
|
103
|
+
* through unchanged — only the cipherstash-installed listener ever
|
|
104
|
+
* rejects with the local sentinel reference, so an `error ===
|
|
105
|
+
* sentinel` identity check after the race is unambiguous.
|
|
106
|
+
*/
|
|
107
|
+
export async function raceCipherstashAbort<T>(
|
|
108
|
+
work: Promise<T>,
|
|
109
|
+
signal: AbortSignal | undefined,
|
|
110
|
+
phase: CipherstashAbortPhase,
|
|
111
|
+
): Promise<T> {
|
|
112
|
+
if (signal === undefined) {
|
|
113
|
+
return await work;
|
|
114
|
+
}
|
|
115
|
+
const sentinel: { reason: unknown } = { reason: undefined };
|
|
116
|
+
let onAbort: (() => void) | undefined;
|
|
117
|
+
|
|
118
|
+
const abortPromise = new Promise<never>((_, reject) => {
|
|
119
|
+
if (signal.aborted) {
|
|
120
|
+
sentinel.reason = signal.reason;
|
|
121
|
+
reject(sentinel);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
onAbort = () => {
|
|
125
|
+
sentinel.reason = signal.reason;
|
|
126
|
+
reject(sentinel);
|
|
127
|
+
};
|
|
128
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
return await Promise.race([work, abortPromise]);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error === sentinel) {
|
|
135
|
+
throw cipherstashAborted(phase, sentinel.reason);
|
|
136
|
+
}
|
|
137
|
+
throw error;
|
|
138
|
+
} finally {
|
|
139
|
+
if (onAbort) {
|
|
140
|
+
signal.removeEventListener('abort', onAbort);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|