@pulse-compute/crypto 1.0.0-beta.4 → 1.0.0-beta.5

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 CHANGED
@@ -2,20 +2,43 @@
2
2
 
3
3
  <!-- pulse-package-status:start -->
4
4
  > **Support tier:** Supported provider/extension surface<br>
5
- > **Audience:** Applications and first-party packages using provider-neutral cryptographic verification.<br>
6
- > **Install directly:** Yes, when an application uses the crypto verification surface directly; JWT applications receive it transitively.<br>
5
+ > **Audience:** Applications and first-party packages using provider-neutral verification and exact-text SHA-256.<br>
6
+ > **Install directly:** Yes, when an application uses verification or digestText directly; JWT applications receive it transitively.<br>
7
7
  > **Supported entry points:** `@pulse-compute/crypto`<br>
8
- > **Stability:** The package root is the supported bounded verification contract; realization and Native integration subpaths remain toolchain-only.<br>
8
+ > **Stability:** The package root supports bounded verification and request-owned exact-text digests; provider, manifest/compiler and Native integration subpaths remain toolchain-only.<br>
9
9
  > **npm:** [`@pulse-compute/crypto`](https://www.npmjs.com/package/@pulse-compute/crypto)<br>
10
- > **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.4/packages/crypto/)
10
+ > **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.5/packages/crypto/)
11
11
  >
12
- > This release-status block is generated from the synchronized `Pulse 1.0.0-beta.4` package policy.
12
+ > This release-status block is generated from the synchronized `Pulse 1.0.0-beta.5` package policy.
13
13
  <!-- pulse-package-status:end -->
14
14
 
15
- Provider-neutral cryptographic verification contracts for Pulse.
15
+ Provider-neutral verification and bounded exact-text SHA-256 for Pulse.
16
16
 
17
17
  This package and `@pulse-compute/jwt` are members of the synchronized
18
- `1.0.0-beta.4` release catalog.
18
+ `1.0.0-beta.5` release catalog.
19
+
20
+ ## Exact-text SHA-256
21
+
22
+ ```ts
23
+ import { crypto } from '@pulse-compute/crypto'
24
+
25
+ const digest = await crypto.digestText(ctx, text)
26
+ ```
27
+
28
+ Select `pulse.crypto: ['SHA-256']`. The current HTTP context owns this effect;
29
+ use a directly awaited local variable or keyed `ctx.parallel` member.
30
+ Success is `{ status: 'ok', sha256, byteLength }`: 64 lowercase hex characters
31
+ and the exact UTF-8 length. The public `DIGEST_TEXT_MAX_BYTES` is 2097152, empty
32
+ input is accepted, and unpaired surrogates are rejected without replacement.
33
+ No normalization, parsing, BOM stripping, or newline conversion occurs.
34
+ Failures are `{ status: 'failed', reason }`, where reason is `invalid-text`,
35
+ `too-large`, `unavailable`, or `realization-failure`; cancellation terminates
36
+ the invocation. The named `digestText` export has the same contract.
37
+
38
+ Node/Fastly Native reuse the selected Crypto SHA-256 guest source. Node/Fastly
39
+ JavaScript use selected Web Crypto. Neither falls back. This bound matches
40
+ current S3 text capacity and leaves larger Catalog envelopes for capacity work.
41
+ See the [exact-text digest contract](https://pulsecompute.io/v1.0.0-beta.5/packages/crypto/#exact-text-digest).
19
42
 
20
43
  ## Verification contract
21
44
 
@@ -132,9 +155,15 @@ deployment.
132
155
  S3 uses Crypto-owned SHA-256 and HMAC-SHA256 byte outputs. Native composes the
133
156
  existing guest-source unit; Node JavaScript explicitly binds Web Crypto through
134
157
  `@pulse-compute/crypto/provider`. The trusted seam snapshots Uint8Array input,
135
- allows at most 32768 data bytes and 8192 HMAC key bytes, returns exactly 32 bytes,
158
+ allows at most 2097152 SHA-256 data bytes, 32768 HMAC data bytes and 8192 HMAC
159
+ key bytes, returns exactly 32 bytes,
136
160
  and wipes input staging on success or failure. It rejects unavailable selected
137
161
  primitives and does not fall back. Empty HMAC keys use the equivalent padded
138
162
  zero block required by HMAC, preserving Native semantics despite Web Crypto's
139
163
  zero-length import restriction. This provider seam does not add an author-facing
140
164
  signing API or change JWT key limits.
165
+
166
+ Large hashes use a separate lazy 2,097,184-byte Native staging frame; small
167
+ hashes and HMAC keep the 40,992-byte frame. Primary-memory Native digest/S3
168
+ modules have a 256 MiB Wasm maximum. Fixed-memory linked guests retain their
169
+ separate ABI and do not establish the 2 MiB text capacity profile.
@@ -0,0 +1,34 @@
1
+ // Crypto-owned text framing over the existing SHA-256 implementation.
2
+ class __PulseTextDigestResult {
3
+ status: string = 'failed'
4
+ reason: string = ''
5
+ sha256: string = ''
6
+ byteLength: i32 = 0
7
+ }
8
+ function __pulse_crypto_digest_text(text: string | null): __PulseTextDigestResult {
9
+ const result = new __PulseTextDigestResult()
10
+ if (text === null) { result.reason = 'invalid-text'; return result }
11
+ if (text.length > 2097152) { result.reason = 'too-large'; return result }
12
+ let length = 0
13
+ for (let i = 0; i < text.length; i++) {
14
+ const c = text.charCodeAt(i)
15
+ if (c >= 0xd800 && c <= 0xdbff) {
16
+ i += 1
17
+ if (i >= text.length) { result.reason = 'invalid-text'; return result }
18
+ const low = text.charCodeAt(i)
19
+ if (low < 0xdc00 || low > 0xdfff) { result.reason = 'invalid-text'; return result }
20
+ length += 4
21
+ } else if (c >= 0xdc00 && c <= 0xdfff) { result.reason = 'invalid-text'; return result }
22
+ else length += c < 0x80 ? 1 : c < 0x800 ? 2 : 3
23
+ }
24
+ if (length > 2097152) { result.reason = 'too-large'; return result }
25
+ const data = Uint8Array.wrap(String.UTF8.encode(text)), digest = new Uint8Array(32)
26
+ const code = pulse_crypto_sha256_bytes_v1(data.dataStart, data.length, digest.dataStart, 32)
27
+ data.fill(0)
28
+ if (code != 1) { digest.fill(0); result.reason = 'realization-failure'; return result }
29
+ const alphabet = '0123456789abcdef'
30
+ for (let i = 0; i < 32; i++) result.sha256 += alphabet.charAt(digest[i] >> 4) + alphabet.charAt(digest[i] & 15)
31
+ digest.fill(0)
32
+ result.status = 'ok'; result.byteLength = length
33
+ return result
34
+ }
@@ -318,12 +318,18 @@ export function pulse_crypto_hs256_verify(
318
318
  // by Crypto; host callers may stage synchronously, copy the output, then wipe.
319
319
  // It does not grow Wasm memory per operation or overlap allocator-owned memory.
320
320
  const __pulse_crypto_bytes_frame = new Uint8Array(8192 + 32768 + 32)
321
+ // Allocate the larger digest-only frame only when a host stages a large hash.
322
+ // Small digest/MAC callers and fixed-memory JWT guests keep the original frame.
323
+ @lazy const __pulse_crypto_digest_frame = new Uint8Array(2097152 + 32)
324
+ export function pulse_crypto_digest_frame_v1(): i32 {
325
+ return <i32>__pulse_crypto_digest_frame.dataStart
326
+ }
321
327
  export function pulse_crypto_bytes_frame_v1(): i32 {
322
328
  return <i32>__pulse_crypto_bytes_frame.dataStart
323
329
  }
324
330
 
325
331
  export function pulse_crypto_sha256_bytes_v1(dataPointer: i32, dataLength: i32, outputPointer: i32, outputLength: i32): i32 {
326
- if (dataLength < 0 || dataLength > 32768 || outputLength != 32
332
+ if (dataLength < 0 || dataLength > 2097152 || outputLength != 32
327
333
  || !__pulse_crypto_memory_range_valid(dataPointer, dataLength)
328
334
  || !__pulse_crypto_memory_range_valid(outputPointer, outputLength)) return __PULSE_CRYPTO_INVALID_INPUT
329
335
  const digest = __pulse_crypto_sha256_memory(<usize><u32>dataPointer, dataLength)
@@ -0,0 +1,38 @@
1
+ {
2
+ "version": "pulse.crypto-text-digest-corpus.v1",
3
+ "algorithm": "SHA-256",
4
+ "encoding": "lowercase-hex",
5
+ "maxUtf8Bytes": 2097152,
6
+ "vectors": [
7
+ {
8
+ "text": "",
9
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
10
+ "byteLength": 0
11
+ },
12
+ {
13
+ "text": "abc",
14
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
15
+ "byteLength": 3
16
+ },
17
+ {
18
+ "text": "\ufeff\u00e9\ud83d\ude00\u0000\r\n",
19
+ "sha256": "894e231659c59a0e7c35180fbca175d0fb836eb20485a8d0dd02bf5ff8c2a4e7",
20
+ "byteLength": 12
21
+ },
22
+ {
23
+ "text": "\u00e9",
24
+ "sha256": "4a99557e4033c3539de2eb65472017cad5f9557f7a0625a09f1c3f6e2ba69c4c",
25
+ "byteLength": 2
26
+ },
27
+ {
28
+ "text": "e\u0301",
29
+ "sha256": "bf12767b0f2a56b2190075bae8169f656e3ce8d6357d4aff184bc6c7ea48f9f6",
30
+ "byteLength": 3
31
+ },
32
+ {
33
+ "text": "{ \"b\":2, \"a\":1 }",
34
+ "sha256": "a782da44b228c14de003efcebc359fbe0a5bc35d90d5da70513e54e25fa5428a",
35
+ "byteLength": 16
36
+ }
37
+ ]
38
+ }
@@ -0,0 +1,85 @@
1
+ {
2
+ "version": "pulse.crypto.hs256-conformance.v2",
3
+ "sources": {
4
+ "sha256": "NIST FIPS 180-4",
5
+ "hmac": "RFC 2104",
6
+ "vectors": "RFC 4231 test cases 6 and 7"
7
+ },
8
+ "resultStatuses": [
9
+ "valid",
10
+ "invalid-authenticator",
11
+ "invalid-key",
12
+ "invalid-input",
13
+ "realization-failure"
14
+ ],
15
+ "resourceLimits": {
16
+ "hmacKeyBytesMinimum": 32,
17
+ "hmacKeyBytesMaximum": 4096,
18
+ "macDataBytesMaximum": 1048576,
19
+ "hs256TagBytes": 32
20
+ },
21
+ "publishedVectors": [
22
+ {
23
+ "id": "rfc4231-6",
24
+ "keyByte": 170,
25
+ "keyLength": 131,
26
+ "dataHex": "54657374205573696e67204c6172676572205468616e20426c6f636b2d53697a65204b6579202d2048617368204b6579204669727374",
27
+ "tagHex": "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54"
28
+ },
29
+ {
30
+ "id": "rfc4231-7",
31
+ "keyByte": 170,
32
+ "keyLength": 131,
33
+ "dataHex": "5468697320697320612074657374207573696e672061206c6172676572207468616e20626c6f636b2d73697a65206b657920616e642061206c6172676572207468616e20626c6f636b2d73697a6520646174612e20546865206b6579206e6565647320746f20626520686173686564206265666f7265206265696e6720757365642062792074686520484d414320616c676f726974686d2e",
34
+ "tagHex": "9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2"
35
+ }
36
+ ],
37
+ "boundaryVectors": {
38
+ "emptyData": {
39
+ "keyByte": 42,
40
+ "keyLength": 32,
41
+ "dataLength": 0,
42
+ "dataByte": 0,
43
+ "tagHex": "25e570b039a294705747f0783f32e9b538be395f8713fbcbf346844d84bc78ad"
44
+ },
45
+ "maximumData": {
46
+ "keyByte": 42,
47
+ "keyLength": 32,
48
+ "dataLength": 1048576,
49
+ "dataByte": 165,
50
+ "tagHex": "c897712d87b7a5649ec1115c6d7461372ff21fdebdc8711b23ddefc0a52e74a3"
51
+ }
52
+ },
53
+ "verificationCases": [
54
+ { "id": "valid-tag", "fixture": "rfc4231-6", "mutation": "none", "expected": "valid" },
55
+ { "id": "invalid-tag", "fixture": "rfc4231-6", "mutation": "zero-tag", "expected": "invalid-authenticator" },
56
+ { "id": "first-byte-mismatch", "fixture": "rfc4231-6", "mutation": "tag-byte-0", "expected": "invalid-authenticator" },
57
+ { "id": "middle-byte-mismatch", "fixture": "rfc4231-6", "mutation": "tag-byte-15", "expected": "invalid-authenticator" },
58
+ { "id": "last-byte-mismatch", "fixture": "rfc4231-6", "mutation": "tag-byte-31", "expected": "invalid-authenticator" },
59
+ { "id": "altered-data", "fixture": "rfc4231-6", "mutation": "data-byte-0", "expected": "invalid-authenticator" },
60
+ { "id": "wrong-key", "fixture": "rfc4231-6", "mutation": "key-byte-0", "expected": "invalid-authenticator" },
61
+ { "id": "undersized-key", "fixture": "rfc4231-6", "mutation": "key-length-31", "expected": "invalid-key" },
62
+ { "id": "malformed-key", "fixture": "rfc4231-6", "mutation": "malformed-key", "expected": "invalid-key" },
63
+ { "id": "empty-data", "fixture": "empty-data", "mutation": "none", "expected": "valid" },
64
+ { "id": "bounded-maximum-data", "fixture": "maximum-data", "mutation": "none", "expected": "valid" },
65
+ { "id": "over-limit-data", "fixture": "maximum-data", "mutation": "data-over-limit", "expected": "invalid-input" },
66
+ { "id": "malformed-tag", "fixture": "rfc4231-6", "mutation": "malformed-tag", "expected": "invalid-input" },
67
+ { "id": "wrong-length-tag", "fixture": "rfc4231-6", "mutation": "tag-length-31", "expected": "invalid-input" }
68
+ ],
69
+ "configurationCases": [
70
+ "inherited-global-crypto",
71
+ "named-profile-replacement",
72
+ "no-array-merging",
73
+ "no-object-merging",
74
+ "empty-array",
75
+ "empty-object",
76
+ "duplicate-array-entry",
77
+ "unknown-key",
78
+ "unknown-algorithm",
79
+ "unknown-realization",
80
+ "profile-missing-hs256",
81
+ "target-without-hs256",
82
+ "unavailable-exact-realization-pin",
83
+ "no-fallback-after-realization-failure"
84
+ ]
85
+ }
@@ -0,0 +1,14 @@
1
+ import type { PulseContext, PulseParallelEffect } from '@pulse-compute/runtime';
2
+ /** Exact UTF-8 text bound, independent of HMAC and JWT limits. */
3
+ export declare const DIGEST_TEXT_MAX_BYTES: 2097152;
4
+ export type TextDigestResult = {
5
+ readonly status: 'ok';
6
+ readonly sha256: string;
7
+ readonly byteLength: number;
8
+ } | {
9
+ readonly status: 'failed';
10
+ readonly reason: 'invalid-text' | 'too-large' | 'unavailable' | 'realization-failure';
11
+ };
12
+ /** Hash exact, well-formed text without normalization, parsing or uploading. */
13
+ export declare function digestText(ctx: PulseContext, text: string): PulseParallelEffect<TextDigestResult>;
14
+ //# sourceMappingURL=digest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../src/digest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAIhF,kEAAkE;AAClE,eAAO,MAAM,qBAAqB,EAAG,OAAgB,CAAC;AACtD,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC/E;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,WAAW,GAAG,aAAa,GAAG,qBAAqB,CAAA;CAAE,CAAC;AAOzH,gFAAgF;AAChF,wBAAgB,UAAU,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,mBAAmB,CAAC,gBAAgB,CAAC,CAMjG"}
package/dist/digest.js ADDED
@@ -0,0 +1,17 @@
1
+ import packageRuntime from '@pulse-compute/runtime/package';
2
+ import digestProvider from '../src/provider.cjs';
3
+ /** Exact UTF-8 text bound, independent of HMAC and JWT limits. */
4
+ export const DIGEST_TEXT_MAX_BYTES = 2097152;
5
+ const runtime = packageRuntime.createPackageRuntime({
6
+ package: '@pulse-compute/crypto', contractId: 'pulse.crypto', providerKind: 'crypto',
7
+ operations: { digestText: { kind: 'crypto.digestText', capability: 'crypto.digestText', result: 'text-digest-result' } },
8
+ });
9
+ /** Hash exact, well-formed text without normalization, parsing or uploading. */
10
+ export function digestText(ctx, text) {
11
+ // Preserve the over-limit signal without copying unbounded text into the
12
+ // shared effect envelope. This prefix is itself over the code-unit bound
13
+ // and is rejected before UTF-8 encoding or hashing on every realization.
14
+ const boundedText = typeof text === 'string' ? text.slice(0, DIGEST_TEXT_MAX_BYTES + 1) : null;
15
+ return runtime.effect(ctx, 'digestText', { text: boundedText }, digestProvider.normalizeTextDigestResult);
16
+ }
17
+ //# sourceMappingURL=digest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"digest.js","sourceRoot":"","sources":["../src/digest.ts"],"names":[],"mappings":"AACA,OAAO,cAAc,MAAM,gCAAgC,CAAC;AAC5D,OAAO,cAAc,MAAM,qBAAqB,CAAC;AAEjD,kEAAkE;AAClE,MAAM,CAAC,MAAM,qBAAqB,GAAG,OAAgB,CAAC;AAKtD,MAAM,OAAO,GAAG,cAAc,CAAC,oBAAoB,CAAC;IAClD,OAAO,EAAE,uBAAuB,EAAE,UAAU,EAAE,cAAc,EAAE,YAAY,EAAE,QAAQ;IACpF,UAAU,EAAE,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,EAAE,oBAAoB,EAAE,EAAE;CAChH,CAAC,CAAC;AAEZ,gFAAgF;AAChF,MAAM,UAAU,UAAU,CAAC,GAAiB,EAAE,IAAY;IACxD,yEAAyE;IACzE,yEAAyE;IACzE,yEAAyE;IACzE,MAAM,WAAW,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,qBAAqB,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/F,OAAO,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,cAAc,CAAC,yBAAyB,CAAC,CAAC;AAC5G,CAAC"}
package/dist/index.d.ts CHANGED
@@ -11,7 +11,10 @@ export declare const mac: Readonly<{
11
11
  export declare const signature: Readonly<{
12
12
  verify: typeof verifySignature;
13
13
  }>;
14
+ export { DIGEST_TEXT_MAX_BYTES, digestText, type TextDigestResult } from './digest.js';
15
+ import { digestText } from './digest.js';
14
16
  export declare const crypto: Readonly<{
17
+ digestText: typeof digestText;
15
18
  mac: Readonly<{
16
19
  verify: typeof verifyMac;
17
20
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,EACrB,6BAA6B,EAC7B,wCAAwC,EACxC,qCAAqC,EACrC,2CAA2C,EAC3C,kCAAkC,EAClC,sBAAsB,EACtB,qCAAqC,EACrC,4BAA4B,EAC5B,KAAK,eAAe,EACpB,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC5B,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,+BAA+B,EAE/B,iCAAiC,EAGlC,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,EACrB,6BAA6B,EAC7B,wCAAwC,EACxC,qCAAqC,EACrC,2CAA2C,EAC3C,kCAAkC,EAClC,sBAAsB,EACtB,qCAAqC,EACrC,4BAA4B,GAC7B,CAAC;AACF,YAAY,EACV,eAAe,EACf,wBAAwB,EACxB,wBAAwB,EACxB,oBAAoB,EACpB,YAAY,EACZ,mBAAmB,EACnB,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,wBAAwB,EACxB,kBAAkB,EAClB,sBAAsB,GACvB,CAAC;AAEF,OAAO,EACL,+BAA+B,EAC/B,iCAAiC,GAClC,CAAC;AAEF,wBAAsB,SAAS,CAC7B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,wBAAwB,CAAC,CAcnC;AAED,wBAAsB,eAAe,CACnC,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,wBAAwB,CAAC,CAenC;AAED,eAAO,MAAM,GAAG;;EAEd,CAAC;AAEH,eAAO,MAAM,SAAS;;EAEpB,CAAC;AAEH,eAAO,MAAM,MAAM;;;;;;;EAGjB,CAAC;AAEH,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,EACrB,6BAA6B,EAC7B,wCAAwC,EACxC,qCAAqC,EACrC,2CAA2C,EAC3C,kCAAkC,EAClC,sBAAsB,EACtB,qCAAqC,EACrC,4BAA4B,EAC5B,KAAK,eAAe,EACpB,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC5B,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,+BAA+B,EAE/B,iCAAiC,EAGlC,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,EACrB,6BAA6B,EAC7B,wCAAwC,EACxC,qCAAqC,EACrC,2CAA2C,EAC3C,kCAAkC,EAClC,sBAAsB,EACtB,qCAAqC,EACrC,4BAA4B,GAC7B,CAAC;AACF,YAAY,EACV,eAAe,EACf,wBAAwB,EACxB,wBAAwB,EACxB,oBAAoB,EACpB,YAAY,EACZ,mBAAmB,EACnB,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,wBAAwB,EACxB,kBAAkB,EAClB,sBAAsB,GACvB,CAAC;AAEF,OAAO,EACL,+BAA+B,EAC/B,iCAAiC,GAClC,CAAC;AAEF,wBAAsB,SAAS,CAC7B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,wBAAwB,CAAC,CAcnC;AAED,wBAAsB,eAAe,CACnC,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,wBAAwB,CAAC,CAenC;AAED,eAAO,MAAM,GAAG;;EAEd,CAAC;AAEH,eAAO,MAAM,SAAS;;EAEpB,CAAC;AAEH,OAAO,EAAE,qBAAqB,EAAE,UAAU,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACvF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,eAAO,MAAM,MAAM;;;;;;;;EAIjB,CAAC;AAEH,eAAe,MAAM,CAAC"}
package/dist/index.js CHANGED
@@ -44,7 +44,10 @@ export const mac = Object.freeze({
44
44
  export const signature = Object.freeze({
45
45
  verify: verifySignature,
46
46
  });
47
+ export { DIGEST_TEXT_MAX_BYTES, digestText } from './digest.js';
48
+ import { digestText } from './digest.js';
47
49
  export const crypto = Object.freeze({
50
+ digestText,
48
51
  mac,
49
52
  signature,
50
53
  });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,EACrB,6BAA6B,EAC7B,wCAAwC,EACxC,qCAAqC,EACrC,2CAA2C,EAC3C,kCAAkC,EAClC,sBAAsB,EACtB,qCAAqC,EACrC,4BAA4B,GAa7B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EACL,+BAA+B,EAC/B,yBAAyB,EACzB,iCAAiC,EACjC,+BAA+B,EAC/B,2BAA2B,GAC5B,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,EACrB,6BAA6B,EAC7B,wCAAwC,EACxC,qCAAqC,EACrC,2CAA2C,EAC3C,kCAAkC,EAClC,sBAAsB,EACtB,qCAAqC,EACrC,4BAA4B,GAC7B,CAAC;AAgBF,OAAO,EACL,+BAA+B,EAC/B,iCAAiC,GAClC,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,OAAyB;IAEzB,IAAI,UAAwD,CAAC;IAC7D,IAAI,CAAC;QACH,UAAU,GAAG,yBAAyB,CAAC,OAAO,CAAC,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,2BAA2B,CAAC,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC,CAAC;IAClE,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,EAAE;QAAE,OAAO,UAAU,CAAC,MAAM,CAAC;IAE7C,IAAI,CAAC;QACH,OAAO,2BAA2B,CAAC,MAAM,WAAW,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,2BAA2B,CAAC,SAAS,CAAC,CAAC;IAChD,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,OAA+B;IAE/B,IAAI,UAA8D,CAAC;IACnE,IAAI,CAAC;QACH,UAAU,GAAG,+BAA+B,CAAC,OAAO,CAAC,CAAC;IACxD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,2BAA2B,CAAC,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC,CAAC;IAClE,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,EAAE;QAAE,OAAO,UAAU,CAAC,MAAM,CAAC;IAE7C,IAAI,CAAC;QACH,OAAO,2BAA2B,CAAC,MAAM,WAAW,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5E,CAAC;IAAC,MAAM,CAAC;QACP,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACrC,OAAO,2BAA2B,CAAC,SAAS,CAAC,CAAC;IAChD,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC;IAC/B,MAAM,EAAE,SAAS;CAClB,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC;IACrC,MAAM,EAAE,eAAe;CACxB,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAClC,GAAG;IACH,SAAS;CACV,CAAC,CAAC;AAEH,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,EACrB,6BAA6B,EAC7B,wCAAwC,EACxC,qCAAqC,EACrC,2CAA2C,EAC3C,kCAAkC,EAClC,sBAAsB,EACtB,qCAAqC,EACrC,4BAA4B,GAa7B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EACL,+BAA+B,EAC/B,yBAAyB,EACzB,iCAAiC,EACjC,+BAA+B,EAC/B,2BAA2B,GAC5B,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,EACrB,6BAA6B,EAC7B,wCAAwC,EACxC,qCAAqC,EACrC,2CAA2C,EAC3C,kCAAkC,EAClC,sBAAsB,EACtB,qCAAqC,EACrC,4BAA4B,GAC7B,CAAC;AAgBF,OAAO,EACL,+BAA+B,EAC/B,iCAAiC,GAClC,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,OAAyB;IAEzB,IAAI,UAAwD,CAAC;IAC7D,IAAI,CAAC;QACH,UAAU,GAAG,yBAAyB,CAAC,OAAO,CAAC,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,2BAA2B,CAAC,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC,CAAC;IAClE,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,EAAE;QAAE,OAAO,UAAU,CAAC,MAAM,CAAC;IAE7C,IAAI,CAAC;QACH,OAAO,2BAA2B,CAAC,MAAM,WAAW,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,2BAA2B,CAAC,SAAS,CAAC,CAAC;IAChD,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,OAA+B;IAE/B,IAAI,UAA8D,CAAC;IACnE,IAAI,CAAC;QACH,UAAU,GAAG,+BAA+B,CAAC,OAAO,CAAC,CAAC;IACxD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,2BAA2B,CAAC,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC,CAAC;IAClE,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,EAAE;QAAE,OAAO,UAAU,CAAC,MAAM,CAAC;IAE7C,IAAI,CAAC;QACH,OAAO,2BAA2B,CAAC,MAAM,WAAW,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5E,CAAC;IAAC,MAAM,CAAC;QACP,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACrC,OAAO,2BAA2B,CAAC,SAAS,CAAC,CAAC;IAChD,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC;IAC/B,MAAM,EAAE,SAAS;CAClB,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC;IACrC,MAAM,EAAE,eAAe;CACxB,CAAC,CAAC;AAEH,OAAO,EAAE,qBAAqB,EAAE,UAAU,EAAyB,MAAM,aAAa,CAAC;AACvF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAClC,UAAU;IACV,GAAG;IACH,SAAS;CACV,CAAC,CAAC;AAEH,eAAe,MAAM,CAAC"}
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@pulse-compute/crypto",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.5",
4
4
  "license": "Apache-2.0",
5
5
  "engines": {
6
6
  "node": "^22.14.0 || ^24.0.0"
7
7
  },
8
- "description": "Provider-neutral cryptographic verification contracts for Pulse.",
8
+ "description": "Provider-neutral cryptographic verification and bounded exact-text digests for Pulse.",
9
9
  "type": "module",
10
10
  "sideEffects": false,
11
11
  "exports": {
@@ -15,7 +15,9 @@
15
15
  "default": "./dist/index.js"
16
16
  },
17
17
  "./pulsewasm-native": "./pulsewasm.native.cjs",
18
- "./provider": "./src/provider.cjs"
18
+ "./provider": "./src/provider.cjs",
19
+ "./pulsewasm/manifest": "./pulsewasm.manifest.cjs",
20
+ "./pulsewasm/compiler": "./pulsewasm.compiler.cjs"
19
21
  },
20
22
  "files": [
21
23
  "as",
@@ -23,6 +25,10 @@
23
25
  "pulsewasm.native.cjs",
24
26
  "README.md",
25
27
  "src/provider.cjs",
28
+ "pulse.package.json",
29
+ "pulsewasm.manifest.cjs",
30
+ "pulsewasm.compiler.cjs",
31
+ "conformance",
26
32
  "LICENSE",
27
33
  "NOTICE"
28
34
  ],
@@ -37,9 +43,19 @@
37
43
  "bugs": {
38
44
  "url": "https://github.com/pulse-compute/pulse/issues"
39
45
  },
40
- "homepage": "https://pulsecompute.io/v1.0.0-beta.4/packages/crypto/",
46
+ "homepage": "https://pulsecompute.io/v1.0.0-beta.5/packages/crypto/",
41
47
  "main": "./dist/index.js",
42
48
  "types": "./dist/index.d.ts",
49
+ "pulse": {
50
+ "contract": "./pulse.package.json"
51
+ },
52
+ "pulsewasm": {
53
+ "manifest": "./pulsewasm.manifest.cjs"
54
+ },
55
+ "dependencies": {
56
+ "@pulse-compute/runtime": "1.0.0-beta.5",
57
+ "@pulse-compute/wasm-contracts": "1.0.0-beta.5"
58
+ },
43
59
  "scripts": {
44
60
  "build": "tsc -b",
45
61
  "test": "corepack pnpm --dir ../.. exec vitest run packages/crypto/test",
@@ -0,0 +1,64 @@
1
+ {
2
+ "version": "pulse.package-contract.v1",
3
+ "kind": "pulse.package-contract",
4
+ "contractId": "pulse.crypto",
5
+ "npmPackage": "@pulse-compute/crypto",
6
+ "description": "Bounded exact-text SHA-256 through an execution-owned effect.",
7
+ "authoring": {
8
+ "namespace": "crypto",
9
+ "import": "@pulse-compute/crypto",
10
+ "symbols": [
11
+ "digestText"
12
+ ]
13
+ },
14
+ "ownership": {
15
+ "profileFragment": "crypto",
16
+ "helperImports": [],
17
+ "reExportImports": []
18
+ },
19
+ "composition": {
20
+ "status": "reserved",
21
+ "helperSymbol": "crypto",
22
+ "profileToken": "opaque-direct-use",
23
+ "reasonCode": "PULSE_PACKAGE_COMPOSITION_HELPER_NOT_REALIZED"
24
+ },
25
+ "targets": {
26
+ "native": {
27
+ "status": "provider-dependent",
28
+ "realization": "canonical-package-lowering",
29
+ "lowerableSubpath": "@pulse-compute/crypto",
30
+ "providerRequirements": [
31
+ "crypto.digestText"
32
+ ],
33
+ "reasonCode": "PULSE_PACKAGE_PROVIDER_CAPABILITY_REQUIRED"
34
+ },
35
+ "javascript": {
36
+ "status": "supported",
37
+ "realization": "javascript-package-runtime",
38
+ "entry": "./dist/index.js",
39
+ "providerRequirements": [
40
+ "crypto.digestText"
41
+ ]
42
+ }
43
+ },
44
+ "lowering": {
45
+ "manifest": "./pulsewasm.manifest.cjs",
46
+ "trust": "first-party",
47
+ "canonicalOperationVersion": "pulse.canonical-package-operation.v1",
48
+ "bundleVersion": "pulse.package-lowering-bundle.v1",
49
+ "providerRequirementVersion": "pulse.provider-requirement-record.v1"
50
+ },
51
+ "conformance": {
52
+ "status": "target-overlap",
53
+ "fixtureRoots": [
54
+ "./conformance"
55
+ ],
56
+ "semanticCases": [
57
+ "crypto.digestText"
58
+ ]
59
+ },
60
+ "docs": {
61
+ "readme": "./README.md",
62
+ "contract": "./pulse.package.json"
63
+ }
64
+ }
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+ const { normalizeDiagnostic, sourceLoc } = require('@pulse-compute/wasm-contracts/diagnostics');
3
+ const { CANONICAL_PACKAGE_EFFECT_VERSION } = require('@pulse-compute/wasm-contracts/handler/canonical-runtime');
4
+ const { PACKAGE_CRYPTO_REQUIREMENT_VERSION } = require('@pulse-compute/wasm-contracts/package/package-contract');
5
+ const PACKAGE = '@pulse-compute/crypto';
6
+ function buildCryptoLoweringPlan(input = {}) {
7
+ const ts = input.typescript || require('typescript');
8
+ const source = input.sourceFile || ts.createSourceFile(input.sourcePath || 'index.ts', input.sourceText || '', ts.ScriptTarget.ES2022, true);
9
+ const namespaces = new Set(), direct = new Set(), diagnostics = [], effects = [];
10
+ const error = (node, suffix, message) => diagnostics.push(normalizeDiagnostic({
11
+ phase: 'crypto-lowering', severity: 'error', code: `PULSEWASM_CRYPTO_DIGEST_${suffix}`, message,
12
+ hint: 'Await crypto.digestText(ctx, text) into a local variable or use it as a direct keyed ctx.parallel member.', loc: sourceLoc(source, node)
13
+ }));
14
+ for (const statement of source.statements) {
15
+ if (!ts.isImportDeclaration(statement) || statement.moduleSpecifier.text !== PACKAGE || statement.importClause?.isTypeOnly) continue;
16
+ if (statement.importClause?.name) namespaces.add(statement.importClause.name.text);
17
+ const named = statement.importClause?.namedBindings;
18
+ if (named && ts.isNamedImports(named)) for (const entry of named.elements) {
19
+ if (entry.isTypeOnly) continue;
20
+ const name = (entry.propertyName || entry.name).text;
21
+ if (name === 'crypto') namespaces.add(entry.name.text);
22
+ if (name === 'digestText') direct.add(entry.name.text);
23
+ }
24
+ }
25
+ function checkBinding(name) {
26
+ if (ts.isIdentifier(name)) {
27
+ if (namespaces.has(name.text) || direct.has(name.text)) error(name, 'SHADOWED', 'Imported Crypto bindings cannot be shadowed.');
28
+ } else if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
29
+ for (const element of name.elements) if (ts.isBindingElement(element)) checkBinding(element.name);
30
+ }
31
+ }
32
+ function isDigest(expression) {
33
+ return ts.isIdentifier(expression) && direct.has(expression.text)
34
+ || ts.isPropertyAccessExpression(expression) && expression.name.text === 'digestText'
35
+ && ts.isIdentifier(expression.expression) && namespaces.has(expression.expression.text);
36
+ }
37
+ // Verification remains an ordinary JavaScript API. Its existing facade
38
+ // aliases and helpers must not acquire digest-specific authoring restrictions.
39
+ let hasDigestUse = false;
40
+ function findDigestUse(node) {
41
+ if (ts.isImportDeclaration(node) || ts.isTypeNode(node)) return;
42
+ if (isDigest(node)
43
+ || ts.isPropertyAccessExpression(node) && node.name.text === 'digestText'
44
+ || ts.isElementAccessExpression(node) && node.argumentExpression && ts.isStringLiteralLike(node.argumentExpression) && node.argumentExpression.text === 'digestText') hasDigestUse = true;
45
+ ts.forEachChild(node, findDigestUse);
46
+ }
47
+ findDigestUse(source);
48
+ function visit(node) {
49
+ if (ts.isTypeNode(node)) return;
50
+ if ((ts.isVariableDeclaration(node) || ts.isParameter(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) && node.name) checkBinding(node.name);
51
+ if (ts.isIdentifier(node) && namespaces.has(node.text)
52
+ && !ts.isImportClause(node.parent) && !ts.isImportSpecifier(node.parent)
53
+ && !(ts.isPropertyAccessExpression(node.parent))) error(node, 'DETACHED', 'The Crypto facade cannot be detached or accessed dynamically.');
54
+ if (isDigest(node) && !ts.isImportSpecifier(node.parent)
55
+ && !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node)
56
+ && (!ts.isCallExpression(node.parent) || node.parent.expression !== node)) error(node, 'DETACHED', 'Digest operations cannot be detached or aliased.');
57
+ if (ts.isCallExpression(node) && isDigest(node.expression)) {
58
+ let fn = node.parent;
59
+ while (fn && !ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn)) fn = fn.parent;
60
+ const ctx = fn && fn.parameters[0] && fn.parameters[0].name;
61
+ if (node.arguments.length !== 2 || !ctx || !ts.isIdentifier(ctx)
62
+ || !ts.isIdentifier(node.arguments[0]) || node.arguments[0].text !== ctx.text) {
63
+ error(node, 'ARGUMENTS', 'Digest requires the current handler context and one text input.'); return;
64
+ }
65
+ let placement = '';
66
+ const parent = node.parent;
67
+ if (ts.isAwaitExpression(parent) && ts.isVariableDeclaration(parent.parent)) placement = 'variable';
68
+ if (ts.isPropertyAssignment(parent) && ts.isObjectLiteralExpression(parent.parent)) {
69
+ const call = parent.parent.parent;
70
+ if (ts.isCallExpression(call) && ts.isPropertyAccessExpression(call.expression)
71
+ && ts.isIdentifier(call.expression.expression) && call.expression.expression.text === ctx.text
72
+ && call.expression.name.text === 'parallel' && ts.isAwaitExpression(call.parent)) placement = 'statement';
73
+ }
74
+ if (!placement) { error(node, 'PLACEMENT', 'Digest must be directly awaited or grouped.'); return; }
75
+ effects.push({ version: CANONICAL_PACKAGE_EFFECT_VERSION, contractId: 'pulse.crypto', package: PACKAGE, import: PACKAGE,
76
+ kind: 'crypto.digestText', capability: 'crypto.digestText', result: 'text-digest-result', providerKind: 'crypto', operation: 'digestText', placement,
77
+ range: { start: node.getStart(source), end: node.getEnd() }, resource: {}, payload: {},
78
+ runtimeInputs: [{ name: 'text', argumentIndex: 1, source: 'package-call-argument' }],
79
+ providerRequirements: ['crypto.digestText'], schemaReferences: [], redaction: ['text', 'sha256'], loc: sourceLoc(source, node) });
80
+ }
81
+ ts.forEachChild(node, visit);
82
+ }
83
+ if (hasDigestUse) visit(source);
84
+ const cryptoRequirements = effects.length ? [{ version: PACKAGE_CRYPTO_REQUIREMENT_VERSION, algorithms: ['SHA-256'], reachable: true, requestedBy: PACKAGE, semanticOwner: PACKAGE }] : [];
85
+ return { artifact: { version: 'pulse.crypto-text-lowering-plan.v1', contractId: 'pulse.crypto', package: PACKAGE, lowerableSubpath: PACKAGE,
86
+ status: diagnostics.length ? 'error' : 'ok', canonicalEffects: effects, cryptoRequirements },
87
+ canonicalEffects: effects, cryptoRequirements, diagnostics, hasErrors: diagnostics.length > 0 };
88
+ }
89
+ module.exports = { buildCryptoLoweringPlan };
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+ module.exports = Object.freeze({
3
+ version: 'pulsewasm.lowerable-library-manifest.v2', kind: 'pulsewasm.lowerable-library-manifest',
4
+ contractId: 'pulse.crypto', npmPackage: '@pulse-compute/crypto', lowerableSubpath: '@pulse-compute/crypto',
5
+ facade: { namespace: 'crypto', import: '@pulse-compute/crypto', symbols: ['digestText'] },
6
+ publicApi: { package: '@pulse-compute/crypto', symbols: ['crypto', 'digestText'], compatibilitySubpaths: [], loweringIntentionallyNarrow: true },
7
+ compiler: { version: 'pulsewasm.lowerable-compiler-builder.v1', entry: './pulsewasm.compiler.cjs', export: 'buildCryptoLoweringPlan', artifact: 'crypto-lowering-plan.json', builderOwner: '@pulse-compute/crypto', trust: 'first-party', inputs: ['typescript.SourceFile'] },
8
+ modes: {
9
+ typescript: { entry: './dist/index.js' }, jsEngine: { entry: './dist/index.js', behavior: 'provider-dependent' },
10
+ wasm: { mode: 'wasm-sidecar', sidecar: './as/digest-text.as.ts',
11
+ lowerings: [{ tsSymbol: 'crypto.digestText', asSymbol: '__pulse_crypto_digest_text', callShape: 'context-runtime-text', hostCapabilities: ['result'] }],
12
+ hostCapabilities: ['result'] }
13
+ },
14
+ policy: { packageOwnsFacadeMapping: true, canonicalRootLowering: true, automaticFallback: false }
15
+ });
@@ -45,6 +45,7 @@ function pulseHmacAssemblyScriptSource(algorithm = 'HS256') {
45
45
  'pulse_crypto_hs256_verify',
46
46
  'pulse_crypto_sha256_digest',
47
47
  'pulse_crypto_bytes_frame_v1',
48
+ 'pulse_crypto_digest_frame_v1',
48
49
  'pulse_crypto_sha256_bytes_v1',
49
50
  'pulse_crypto_hmac_sha256_bytes_v1'
50
51
  ]),
@@ -55,8 +56,10 @@ function pulseHmacAssemblyScriptSource(algorithm = 'HS256') {
55
56
  hs256TagBytes: 32,
56
57
  byteOperationKeyBytesMaximum: 8192,
57
58
  byteOperationDataBytesMaximum: 32768,
59
+ digestDataBytesMaximum: 2097152,
58
60
  byteOperationOutputBytes: 32,
59
- byteOperationFrameBytes: 40992
61
+ byteOperationFrameBytes: 40992,
62
+ digestFrameBytes: 2097184
60
63
  }),
61
64
  resultCodes: Object.freeze({
62
65
  valid: 1,
@@ -76,20 +79,23 @@ function pulseHmacAssemblyScriptSource(algorithm = 'HS256') {
76
79
 
77
80
  function bindNativeDigestMac(moduleExports) {
78
81
  const memory = moduleExports && moduleExports.memory;
79
- const names = ['pulse_crypto_bytes_frame_v1', 'pulse_crypto_sha256_bytes_v1', 'pulse_crypto_hmac_sha256_bytes_v1'];
82
+ const names = ['pulse_crypto_bytes_frame_v1', 'pulse_crypto_digest_frame_v1', 'pulse_crypto_sha256_bytes_v1', 'pulse_crypto_hmac_sha256_bytes_v1'];
80
83
  if (!(memory instanceof WebAssembly.Memory) || names.some((name) => typeof moduleExports[name] !== 'function')) {
81
84
  throw new TypeError('Native Crypto digest/MAC exports are unavailable.');
82
85
  }
83
- const frameBytes = 8192 + 32768 + 32;
84
86
  function run(data, key) {
85
- if (!(data instanceof Uint8Array) || data.length > 32768
87
+ if (!(data instanceof Uint8Array) || data.length > (key === undefined ? 2097152 : 32768)
86
88
  || (key !== undefined && (!(key instanceof Uint8Array) || key.length > 8192))) {
87
89
  throw new TypeError('Native Crypto digest/MAC input exceeds its byte contract.');
88
90
  }
89
- const pointer = moduleExports.pulse_crypto_bytes_frame_v1() >>> 0;
91
+ const largeDigest = key === undefined && data.length > 32768;
92
+ const dataBytes = largeDigest ? 2097152 : 32768;
93
+ const keyBytes = largeDigest ? 0 : 8192;
94
+ const frameBytes = keyBytes + dataBytes + 32;
95
+ const pointer = (largeDigest ? moduleExports.pulse_crypto_digest_frame_v1() : moduleExports.pulse_crypto_bytes_frame_v1()) >>> 0;
90
96
  if (pointer + frameBytes > memory.buffer.byteLength) throw new TypeError('Native Crypto frame is outside memory.');
91
- const dataPointer = pointer + 8192;
92
- const outputPointer = dataPointer + 32768;
97
+ const dataPointer = pointer + keyBytes;
98
+ const outputPointer = dataPointer + dataBytes;
93
99
  try {
94
100
  const view = new Uint8Array(memory.buffer);
95
101
  view.set(data, dataPointer);
package/src/provider.cjs CHANGED
@@ -19,7 +19,7 @@ function bindJavascriptDigestMac(algorithms, subtle = globalThis.crypto && globa
19
19
  || algorithms.includes('HMAC-SHA256') && (typeof subtle.sign !== 'function' || typeof subtle.importKey !== 'function')) throw new TypeError('Selected Web Crypto byte realization is unavailable.');
20
20
  const bytes = {};
21
21
  if (algorithms.includes('SHA-256')) bytes.sha256 = async (input) => {
22
- const data = snapshot(input, 32768);
22
+ const data = snapshot(input, 2097152);
23
23
  try { return result(await subtle.digest({ name: 'SHA-256' }, data)); }
24
24
  finally { data.fill(0); }
25
25
  };
@@ -40,4 +40,73 @@ function bindJavascriptDigestMac(algorithms, subtle = globalThis.crypto && globa
40
40
  algorithms: Object.freeze(algorithms.map((algorithm) => Object.freeze({ algorithm, realization: 'runtime-builtin', implementation: IMPLEMENTATIONS[algorithm], available: true, automaticFallback: false })))
41
41
  }) });
42
42
  }
43
- module.exports = Object.freeze({ IMPLEMENTATIONS, bindJavascriptDigestMac });
43
+ const DIGEST_TEXT_MAX_BYTES = 2097152;
44
+ const digestFailures = Object.freeze(Object.fromEntries(
45
+ ['invalid-text', 'too-large', 'unavailable', 'realization-failure'].map(reason => [reason, Object.freeze({ status: 'failed', reason })])
46
+ ));
47
+ // Count scalar UTF-8 bytes before allocating staging; do not replace surrogates.
48
+ function textDigestByteLength(text) {
49
+ if (typeof text !== 'string') return -1;
50
+ if (text.length > DIGEST_TEXT_MAX_BYTES) return -2;
51
+ let bytes = 0;
52
+ for (let i = 0; i < text.length; i++) {
53
+ const c = text.charCodeAt(i);
54
+ if (c >= 0xd800 && c <= 0xdbff) {
55
+ const low = text.charCodeAt(++i);
56
+ if (!(low >= 0xdc00 && low <= 0xdfff)) return -1;
57
+ bytes += 4;
58
+ } else if (c >= 0xdc00 && c <= 0xdfff) return -1;
59
+ else bytes += c < 0x80 ? 1 : c < 0x800 ? 2 : 3;
60
+ }
61
+ return bytes > DIGEST_TEXT_MAX_BYTES ? -2 : bytes;
62
+ }
63
+ function normalizeTextDigestResult(value) {
64
+ const failed = digestFailures['realization-failure'];
65
+ if (!value || typeof value !== 'object' || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) return failed;
66
+ const properties = Object.getOwnPropertyDescriptors(value), keys = Reflect.ownKeys(properties);
67
+ if (keys.some(key => typeof key !== 'string' || !properties[key].enumerable || !Object.hasOwn(properties[key], 'value'))) return failed;
68
+ const status = properties.status?.value, reason = properties.reason?.value;
69
+ if (status === 'failed' && keys.length === 2 && typeof reason === 'string' && Object.hasOwn(digestFailures, reason)) return digestFailures[reason];
70
+ const sha256 = properties.sha256?.value, byteLength = properties.byteLength?.value;
71
+ if (status !== 'ok' || keys.length !== 3 || typeof sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(sha256)
72
+ || !Number.isInteger(byteLength) || byteLength < 0 || byteLength > DIGEST_TEXT_MAX_BYTES) return failed;
73
+ return Object.freeze({ status: 'ok', sha256, byteLength });
74
+ }
75
+ function checkDigestCancellation(signal) {
76
+ if (signal?.aborted) throw signal.reason || new Error('Pulse digest invocation cancelled.');
77
+ }
78
+ async function executeTextDigest(effect, options = {}) {
79
+ if (!effect || effect.package !== '@pulse-compute/crypto' || effect.contractId !== 'pulse.crypto'
80
+ || effect.providerKind !== 'crypto' || effect.operation !== 'digestText'
81
+ || effect.kind !== 'crypto.digestText' || effect.capability !== 'crypto.digestText') throw new TypeError('Invalid Crypto digest effect authority.');
82
+ checkDigestCancellation(options.signal);
83
+ const text = effect.payload?.text, byteLength = textDigestByteLength(text);
84
+ if (byteLength < 0) return digestFailures[byteLength === -1 ? 'invalid-text' : 'too-large'];
85
+ const realization = options.cryptoTarget === 'javascript' ? 'runtime-builtin' : 'guest-source:pulse-hmac-as';
86
+ const bytes = options.cryptoVerifier?.bytes, algorithms = options.cryptoRealization?.algorithms;
87
+ if (typeof bytes?.sha256 !== 'function' || !Array.isArray(algorithms)
88
+ || !algorithms.some(entry => entry.algorithm === 'SHA-256' && entry.available === true && entry.realization === realization)) return digestFailures.unavailable;
89
+ const data = new TextEncoder().encode(text);
90
+ let digest;
91
+ try {
92
+ digest = await bytes.sha256(data);
93
+ checkDigestCancellation(options.signal);
94
+ if (!(digest instanceof Uint8Array) || digest.length !== 32) return digestFailures['realization-failure'];
95
+ const sha256 = Array.from(digest, byte => byte.toString(16).padStart(2, '0')).join('');
96
+ return Object.freeze({ status: 'ok', sha256, byteLength });
97
+ } catch (error) {
98
+ checkDigestCancellation(options.signal);
99
+ return digestFailures['realization-failure'];
100
+ } finally { data.fill(0); if (digest instanceof Uint8Array) digest.fill(0); }
101
+ }
102
+ function createJavascriptTextDigest(options = {}) {
103
+ return (effect, execution = {}) => {
104
+ let selected;
105
+ try { selected = bindJavascriptDigestMac(['SHA-256'], options.digestSubtle === undefined ? globalThis.crypto?.subtle : options.digestSubtle); }
106
+ catch { selected = undefined; }
107
+ return executeTextDigest(effect, { signal: execution.signal, cryptoTarget: 'javascript',
108
+ cryptoVerifier: selected, cryptoRealization: selected?.realization });
109
+ };
110
+ }
111
+ module.exports = Object.freeze({ IMPLEMENTATIONS, bindJavascriptDigestMac, DIGEST_TEXT_MAX_BYTES,
112
+ textDigestByteLength, normalizeTextDigestResult, executeTextDigest, createJavascriptTextDigest });