@pulse-compute/runtime 0.0.0 → 1.0.0-beta.2
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 +126 -2
- package/docs/API.md +572 -0
- package/docs/guides/routing.md +202 -0
- package/docs/preview-scope.md +142 -0
- package/package.json +38 -5
- package/src/host.d.ts +420 -0
- package/src/host.js +115 -0
- package/src/index.d.ts +215 -0
- package/src/index.js +11 -0
- package/src/internal/bindings.js +281 -0
- package/src/internal/body.js +254 -0
- package/src/internal/conditional-kv.js +199 -0
- package/src/internal/context.js +275 -0
- package/src/internal/effect-adapter.js +877 -0
- package/src/internal/errors.js +27 -0
- package/src/internal/event-emission.js +182 -0
- package/src/internal/event-execution.js +496 -0
- package/src/internal/fetch.js +221 -0
- package/src/internal/index.js +24 -0
- package/src/internal/logging.js +65 -0
- package/src/internal/package-runtime.js +604 -0
- package/src/internal/path.js +130 -0
- package/src/internal/redaction.js +231 -0
- package/src/internal/response.js +415 -0
- package/src/internal/router.js +487 -0
- package/src/internal/schema.js +340 -0
- package/src/package.d.ts +127 -0
- package/src/package.js +19 -0
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
export type HeaderPair = readonly [name: string, value: string];
|
|
2
|
+
|
|
3
|
+
declare const pulseEffectBrand: unique symbol;
|
|
4
|
+
declare const pulseParallelEffectBrand: unique symbol;
|
|
5
|
+
|
|
6
|
+
export interface PulseEffect<T> extends Promise<T> {
|
|
7
|
+
readonly [pulseEffectBrand]: T;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** An externally dispatched Pulse effect that may be a member of ctx.parallel(). */
|
|
11
|
+
export interface PulseParallelEffect<T> extends PulseEffect<T> {
|
|
12
|
+
readonly [pulseParallelEffectBrand]: true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type PulseEffectResult<T> = T extends PulseEffect<infer Result> ? Result : never;
|
|
16
|
+
export type PulseParallelResult<T extends Readonly<Record<string, PulseParallelEffect<unknown>>>> = {
|
|
17
|
+
readonly [Key in keyof T]: PulseEffectResult<T[Key]>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface PulseRequest {
|
|
21
|
+
readonly method: string;
|
|
22
|
+
readonly url: string;
|
|
23
|
+
readonly path: string;
|
|
24
|
+
readonly headers: readonly HeaderPair[];
|
|
25
|
+
header(name: string): string | undefined;
|
|
26
|
+
text(): PulseEffect<string>;
|
|
27
|
+
json<T = unknown>(schemaId?: string): PulseEffect<T>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PulseFetchInitBase {
|
|
31
|
+
readonly method?: 'GET' | 'HEAD' | 'POST';
|
|
32
|
+
readonly headers?: Readonly<Record<string, string>> | readonly HeaderPair[];
|
|
33
|
+
readonly timeoutMs?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type PulseFetchInit = PulseFetchInitBase & (
|
|
37
|
+
| { readonly body?: never; readonly json?: never; readonly schema?: never }
|
|
38
|
+
| { readonly body: string; readonly json?: never; readonly schema?: never }
|
|
39
|
+
| { readonly body?: never; readonly json: unknown; readonly schema?: string }
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export interface PulseStructuredResponse {
|
|
43
|
+
readonly status: number;
|
|
44
|
+
readonly ok: boolean;
|
|
45
|
+
readonly kind: string;
|
|
46
|
+
readonly bodyClass: 'structured';
|
|
47
|
+
readonly headers: readonly HeaderPair[];
|
|
48
|
+
header(name: string): string | undefined;
|
|
49
|
+
text(): PulseEffect<string>;
|
|
50
|
+
json<T = unknown>(schemaId?: string): PulseEffect<T>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface PulseOpaqueResponse {
|
|
54
|
+
readonly status: number;
|
|
55
|
+
readonly ok: boolean;
|
|
56
|
+
readonly kind: 'stream';
|
|
57
|
+
readonly bodyClass: 'opaque';
|
|
58
|
+
readonly headers: readonly HeaderPair[];
|
|
59
|
+
header(name: string): string | undefined;
|
|
60
|
+
/** Opaque host-owned handle. It is returnable but not inspectable or iterable. */
|
|
61
|
+
readonly bodyHandle: unknown;
|
|
62
|
+
/** Opaque pass-through bodies cannot be inspected. */
|
|
63
|
+
text(): never;
|
|
64
|
+
/** Opaque pass-through bodies cannot be inspected. */
|
|
65
|
+
json<T = unknown>(schemaId?: string): never;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type PulseFetchResponse = PulseStructuredResponse | PulseOpaqueResponse;
|
|
69
|
+
|
|
70
|
+
export interface PulseFetchOperation extends PulseParallelEffect<PulseOpaqueResponse> {
|
|
71
|
+
text(): PulseParallelEffect<string>;
|
|
72
|
+
json<T = unknown>(schemaId?: string): PulseParallelEffect<T>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface PulseResult {
|
|
76
|
+
readonly status: number;
|
|
77
|
+
readonly kind: string;
|
|
78
|
+
readonly bodyClass: 'structured' | 'opaque';
|
|
79
|
+
readonly headers: readonly HeaderPair[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface PulseResponseOptions {
|
|
83
|
+
readonly status?: number;
|
|
84
|
+
readonly headers?: Readonly<Record<string, string>> | readonly HeaderPair[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface PulseJsonResponseOptions extends PulseResponseOptions {
|
|
88
|
+
/** Exact schema ID declared by the project. */
|
|
89
|
+
readonly schema?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface PulseState {
|
|
93
|
+
get(key: string): string | undefined;
|
|
94
|
+
set(key: string, value: string): void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface PulseLogger {
|
|
98
|
+
error(message: string): void;
|
|
99
|
+
warn(message: string): void;
|
|
100
|
+
info(message: string): void;
|
|
101
|
+
debug(message: string): void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Opaque, bounded string. It is neither an application version nor a number.
|
|
105
|
+
export type PulseKvGeneration = string;
|
|
106
|
+
export type PulseKvReadFailureReason =
|
|
107
|
+
| 'invalid-key' | 'configuration' | 'not-authorized' | 'throttled'
|
|
108
|
+
| 'unavailable' | 'transport' | 'timeout' | 'protocol' | 'too-large' | 'invalid-value';
|
|
109
|
+
export type PulseKvVersionedResult<T> =
|
|
110
|
+
| { readonly status: 'found'; readonly value: T; readonly generation: PulseKvGeneration }
|
|
111
|
+
| { readonly status: 'not-found' }
|
|
112
|
+
| { readonly status: 'failed'; readonly reason: PulseKvReadFailureReason };
|
|
113
|
+
|
|
114
|
+
export type PulseKvNotStoredReason =
|
|
115
|
+
| 'invalid-key' | 'invalid-value' | 'invalid-generation' | 'too-large'
|
|
116
|
+
| 'configuration' | 'not-authorized' | 'throttled' | 'rejected'
|
|
117
|
+
| 'unavailable' | 'transport' | 'timeout' | 'protocol';
|
|
118
|
+
export type PulseKvConditionalResult =
|
|
119
|
+
| { readonly status: 'stored' }
|
|
120
|
+
| { readonly status: 'conflict' }
|
|
121
|
+
| { readonly status: 'not-stored'; readonly reason: PulseKvNotStoredReason }
|
|
122
|
+
| { readonly status: 'unknown'; readonly reason: 'transport' | 'timeout' | 'unavailable' | 'protocol' };
|
|
123
|
+
|
|
124
|
+
export interface PulseKvNamespace<T = unknown> {
|
|
125
|
+
getVersioned(key: string): PulseParallelEffect<PulseKvVersionedResult<T>>;
|
|
126
|
+
insertIfAbsent(key: string, value: T): PulseParallelEffect<PulseKvConditionalResult>;
|
|
127
|
+
compareAndSwap(key: string, generation: PulseKvGeneration, value: T): PulseParallelEffect<PulseKvConditionalResult>;
|
|
128
|
+
get(key: string): PulseParallelEffect<T | undefined>;
|
|
129
|
+
put(key: string, value: T): PulseParallelEffect<boolean>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export type PulseEmitEvent<Payload = unknown> =
|
|
133
|
+
| Readonly<{ schema: string; payload: Payload }>
|
|
134
|
+
| Readonly<{ schema: null; payload?: never }>;
|
|
135
|
+
|
|
136
|
+
/** Plane-neutral authority shared by one isolated HTTP request or event invocation. */
|
|
137
|
+
export interface PulseExecutionContext {
|
|
138
|
+
readonly state: PulseState;
|
|
139
|
+
readonly log: PulseLogger;
|
|
140
|
+
fetch(url: string, init?: PulseFetchInit): PulseFetchOperation;
|
|
141
|
+
/** Dispatches a statically keyed group of independent Pulse effects and rejoins in property order. */
|
|
142
|
+
parallel<const T extends Readonly<Record<string, PulseParallelEffect<unknown>>>>(effects: T): PulseEffect<PulseParallelResult<T>>;
|
|
143
|
+
readonly config: { get(name: string): PulseParallelEffect<string | undefined> };
|
|
144
|
+
readonly secret: { get(name: string): PulseParallelEffect<string | undefined> };
|
|
145
|
+
kv<T = unknown>(name: string): PulseKvNamespace<T>;
|
|
146
|
+
emit<Payload = unknown>(
|
|
147
|
+
type: string,
|
|
148
|
+
event: Readonly<{ schema: string; payload: Payload }>
|
|
149
|
+
): PulseParallelEffect<void>;
|
|
150
|
+
emit(
|
|
151
|
+
type: string,
|
|
152
|
+
event: Readonly<{ schema: null; payload?: never }>
|
|
153
|
+
): PulseParallelEffect<void>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** HTTP request execution context. */
|
|
157
|
+
export interface PulseContext extends PulseExecutionContext {
|
|
158
|
+
readonly req: PulseRequest;
|
|
159
|
+
json(value: unknown, descriptor?: PulseJsonResponseOptions | string): PulseResult;
|
|
160
|
+
text(value: string, options?: PulseResponseOptions): PulseResult;
|
|
161
|
+
response(input?: PulseResponseOptions & { readonly body?: string }): PulseResult;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface PulseRouteContext extends PulseContext {
|
|
165
|
+
/** Returns the named static route parameter for the matched Router path. */
|
|
166
|
+
param(name: string): string | undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface PulseEvent<Payload = unknown> {
|
|
170
|
+
readonly type: string;
|
|
171
|
+
readonly payload: Payload;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Non-HTTP event execution context. Event handlers cannot access request or response fields. */
|
|
175
|
+
export interface PulseEventContext<Payload = unknown> extends PulseExecutionContext {
|
|
176
|
+
readonly event: PulseEvent<Payload>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export type PulseEventHandler<Payload = unknown> = (ctx: PulseEventContext<Payload>) => Promise<void>;
|
|
180
|
+
|
|
181
|
+
export type HandlerResult = PulseResult | PulseFetchResponse;
|
|
182
|
+
export type Handler = (ctx: PulseContext) => Promise<HandlerResult>;
|
|
183
|
+
/** Terminal Router control transfer. The current handler never resumes after this value is returned. */
|
|
184
|
+
export type RouterNext = (error?: unknown) => never;
|
|
185
|
+
export type RouteHandler = (ctx: PulseRouteContext, next: RouterNext) => Promise<HandlerResult>;
|
|
186
|
+
export type RouterMiddleware = (ctx: PulseContext, next: RouterNext) => Promise<HandlerResult>;
|
|
187
|
+
export type RouterErrorHandler = (error: unknown, ctx: PulseContext, next: RouterNext) => Promise<HandlerResult>;
|
|
188
|
+
|
|
189
|
+
/** Canonical Pulse-prefixed aliases. They do not create a second handler or Router algebra. */
|
|
190
|
+
export type PulseHandlerResult = HandlerResult;
|
|
191
|
+
export type PulseHandler = Handler;
|
|
192
|
+
export type PulseRouteHandler = RouteHandler;
|
|
193
|
+
export type PulseMiddleware = RouterMiddleware;
|
|
194
|
+
export type PulseErrorHandler = RouterErrorHandler;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Static authoring marker compiled by Pulse. Router instances are not runtime dispatchers.
|
|
198
|
+
* Canonical v2 supports terminal middleware/error transfer, get/head/post/put/patch/delete routes,
|
|
199
|
+
* fallthrough, and acyclic static mounts.
|
|
200
|
+
*/
|
|
201
|
+
export declare class Router {
|
|
202
|
+
use(handler: RouterMiddleware): this;
|
|
203
|
+
use(path: string, handler: RouterMiddleware): this;
|
|
204
|
+
get(path: string, handler: RouteHandler): this;
|
|
205
|
+
head(path: string, handler: RouteHandler): this;
|
|
206
|
+
post(path: string, handler: RouteHandler): this;
|
|
207
|
+
put(path: string, handler: RouteHandler): this;
|
|
208
|
+
patch(path: string, handler: RouteHandler): this;
|
|
209
|
+
delete(path: string, handler: RouteHandler): this;
|
|
210
|
+
mount(path: string, router: Router): this;
|
|
211
|
+
error(handler: RouterErrorHandler): this;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export declare const RUNTIME_API_VERSION: 'pulse.runtime-authoring.v4';
|
|
215
|
+
export declare const ROUTER_API_VERSION: 'pulse.router-authoring.v2';
|
package/src/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Router: LiveRouter } = require('./internal/router.js');
|
|
4
|
+
|
|
5
|
+
const RUNTIME_API_VERSION = 'pulse.runtime-authoring.v4';
|
|
6
|
+
const ROUTER_API_VERSION = 'pulse.router-authoring.v2';
|
|
7
|
+
|
|
8
|
+
/** Public authoring value backed by the package-internal live Router realization. */
|
|
9
|
+
class Router extends LiveRouter {}
|
|
10
|
+
|
|
11
|
+
module.exports = Object.freeze({ RUNTIME_API_VERSION, ROUTER_API_VERSION, Router });
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { PulseRuntimeContractError } = require('./errors.js');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MAX_BINDING_NAME_BYTES = 256;
|
|
6
|
+
const DEFAULT_MAX_BINDING_VALUE_BYTES = 65_536;
|
|
7
|
+
const DEFAULT_MAX_KV_NAMESPACE_BYTES = 256;
|
|
8
|
+
const DEFAULT_MAX_KV_KEY_BYTES = 1_024;
|
|
9
|
+
const DEFAULT_MAX_KV_VALUE_BYTES = 65_536;
|
|
10
|
+
const DEFAULT_MAX_KV_VALUE_DEPTH = 64;
|
|
11
|
+
const DEFAULT_MAX_KV_VALUE_ENTRIES = 10_000;
|
|
12
|
+
const textEncoder = new TextEncoder();
|
|
13
|
+
|
|
14
|
+
function byteLength(value) {
|
|
15
|
+
return textEncoder.encode(String(value)).byteLength;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function positiveLimit(label, value, fallback) {
|
|
19
|
+
const normalized = value === undefined ? fallback : Number(value);
|
|
20
|
+
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
|
|
21
|
+
throw new TypeError(`Pulse ${label} must be a positive safe integer.`);
|
|
22
|
+
}
|
|
23
|
+
return normalized;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function contractError(code, message, detail) {
|
|
27
|
+
return new PulseRuntimeContractError(code, message, {
|
|
28
|
+
detail: detail === undefined ? undefined : Object.freeze({ ...detail })
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function boundedString(label, value, options) {
|
|
33
|
+
const {
|
|
34
|
+
invalidCode,
|
|
35
|
+
tooLargeCode,
|
|
36
|
+
maxBytes,
|
|
37
|
+
detail = {}
|
|
38
|
+
} = options;
|
|
39
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
40
|
+
throw contractError(invalidCode, `Pulse ${label} must be a non-empty string.`, {
|
|
41
|
+
...detail,
|
|
42
|
+
valueType: typeof value
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const bytes = byteLength(value);
|
|
46
|
+
if (bytes > maxBytes) {
|
|
47
|
+
throw contractError(tooLargeCode, `Pulse ${label} exceeds the ${maxBytes} byte limit.`, {
|
|
48
|
+
...detail,
|
|
49
|
+
bytes,
|
|
50
|
+
maxBytes
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeBindingName(kind, value, options = {}) {
|
|
57
|
+
const bindingKind = String(kind || 'binding');
|
|
58
|
+
return boundedString(`${bindingKind} name`, value, {
|
|
59
|
+
invalidCode: 'PULSE_BINDING_NAME_INVALID',
|
|
60
|
+
tooLargeCode: 'PULSE_BINDING_NAME_TOO_LARGE',
|
|
61
|
+
maxBytes: positiveLimit('maxBindingNameBytes', options.maxBindingNameBytes, DEFAULT_MAX_BINDING_NAME_BYTES),
|
|
62
|
+
detail: { bindingKind }
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeKvNamespace(value, options = {}) {
|
|
67
|
+
return boundedString('KV namespace', value, {
|
|
68
|
+
invalidCode: 'PULSE_KV_NAMESPACE_INVALID',
|
|
69
|
+
tooLargeCode: 'PULSE_KV_NAMESPACE_TOO_LARGE',
|
|
70
|
+
maxBytes: positiveLimit('maxKvNamespaceBytes', options.maxKvNamespaceBytes, DEFAULT_MAX_KV_NAMESPACE_BYTES)
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeKvKey(value, options = {}) {
|
|
75
|
+
return boundedString('KV key', value, {
|
|
76
|
+
invalidCode: 'PULSE_KV_KEY_INVALID',
|
|
77
|
+
tooLargeCode: 'PULSE_KV_KEY_TOO_LARGE',
|
|
78
|
+
maxBytes: positiveLimit('maxKvKeyBytes', options.maxKvKeyBytes, DEFAULT_MAX_KV_KEY_BYTES)
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeBindingValueInternal(kind, value, options = {}) {
|
|
83
|
+
if (value === undefined) return undefined;
|
|
84
|
+
const bindingKind = String(kind || 'binding');
|
|
85
|
+
if (typeof value !== 'string') {
|
|
86
|
+
throw contractError(
|
|
87
|
+
'PULSE_BINDING_VALUE_INVALID',
|
|
88
|
+
`Pulse ${bindingKind} providers must return a string or undefined.`,
|
|
89
|
+
{ bindingKind, valueType: value === null ? 'null' : typeof value }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const maxBytes = positiveLimit('maxBindingValueBytes', options.maxBindingValueBytes, DEFAULT_MAX_BINDING_VALUE_BYTES);
|
|
93
|
+
const bytes = byteLength(value);
|
|
94
|
+
if (bytes > maxBytes) {
|
|
95
|
+
throw contractError(
|
|
96
|
+
'PULSE_BINDING_VALUE_TOO_LARGE',
|
|
97
|
+
`Pulse ${bindingKind} value exceeds the ${maxBytes} byte limit.`,
|
|
98
|
+
{ bindingKind, bytes, maxBytes }
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function valueType(value) {
|
|
105
|
+
if (value === null) return 'null';
|
|
106
|
+
if (Array.isArray(value)) return 'array';
|
|
107
|
+
return typeof value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function kvValueError(message, path, value, extra = {}) {
|
|
111
|
+
return contractError('PULSE_KV_VALUE_INVALID', message, {
|
|
112
|
+
path,
|
|
113
|
+
valueType: valueType(value),
|
|
114
|
+
...extra
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function definePortableProperty(target, key, value) {
|
|
119
|
+
Object.defineProperty(target, key, {
|
|
120
|
+
enumerable: true,
|
|
121
|
+
configurable: false,
|
|
122
|
+
writable: false,
|
|
123
|
+
value
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function clonePortableKvValue(value, state, path, depth) {
|
|
128
|
+
state.entries += 1;
|
|
129
|
+
if (state.entries > state.maxEntries) {
|
|
130
|
+
throw contractError(
|
|
131
|
+
'PULSE_KV_VALUE_ENTRIES_EXCEEDED',
|
|
132
|
+
`Pulse KV values may not exceed ${state.maxEntries} entries.`,
|
|
133
|
+
{ path, maxEntries: state.maxEntries }
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (depth > state.maxDepth) {
|
|
137
|
+
throw contractError(
|
|
138
|
+
'PULSE_KV_VALUE_DEPTH_EXCEEDED',
|
|
139
|
+
`Pulse KV values may not exceed ${state.maxDepth} nested levels.`,
|
|
140
|
+
{ path, maxDepth: state.maxDepth }
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;
|
|
144
|
+
if (typeof value === 'number') {
|
|
145
|
+
if (!Number.isFinite(value)) throw kvValueError('Pulse KV numbers must be finite.', path, value);
|
|
146
|
+
return Object.is(value, -0) ? 0 : value;
|
|
147
|
+
}
|
|
148
|
+
if (value === undefined && state.allowUndefined && path === '$') return undefined;
|
|
149
|
+
if (value === undefined) throw kvValueError('Pulse KV values may not contain undefined.', path, value);
|
|
150
|
+
if (typeof value !== 'object') {
|
|
151
|
+
throw kvValueError('Pulse KV values must be JSON-compatible data.', path, value);
|
|
152
|
+
}
|
|
153
|
+
if (state.seen.has(value)) {
|
|
154
|
+
throw kvValueError('Pulse KV values may not contain cycles or repeated object references.', path, value);
|
|
155
|
+
}
|
|
156
|
+
state.seen.add(value);
|
|
157
|
+
|
|
158
|
+
if (Array.isArray(value)) {
|
|
159
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
160
|
+
const allowed = new Set(['length']);
|
|
161
|
+
for (let index = 0; index < value.length; index += 1) allowed.add(String(index));
|
|
162
|
+
for (const key of ownKeys) {
|
|
163
|
+
if (typeof key !== 'string' || !allowed.has(key)) {
|
|
164
|
+
throw kvValueError('Pulse KV arrays may only contain indexed data properties.', path, value, { key: String(key) });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const output = [];
|
|
168
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
169
|
+
if (!Object.prototype.hasOwnProperty.call(value, index)) {
|
|
170
|
+
throw kvValueError('Pulse KV arrays may not contain sparse entries.', `${path}[${index}]`, undefined);
|
|
171
|
+
}
|
|
172
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
173
|
+
if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
174
|
+
throw kvValueError('Pulse KV arrays may not contain accessors.', `${path}[${index}]`, undefined);
|
|
175
|
+
}
|
|
176
|
+
definePortableProperty(output, String(index), clonePortableKvValue(descriptor.value, state, `${path}[${index}]`, depth + 1));
|
|
177
|
+
}
|
|
178
|
+
Object.defineProperty(output, 'length', { writable: false });
|
|
179
|
+
return Object.freeze(output);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const prototype = Object.getPrototypeOf(value);
|
|
183
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
184
|
+
throw kvValueError('Pulse KV objects must be ordinary object literals.', path, value, {
|
|
185
|
+
prototype: prototype && prototype.constructor && prototype.constructor.name || 'null'
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const output = {};
|
|
189
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
190
|
+
if (typeof key !== 'string') {
|
|
191
|
+
throw kvValueError('Pulse KV objects may not contain symbol keys.', path, value);
|
|
192
|
+
}
|
|
193
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
194
|
+
if (!descriptor || !descriptor.enumerable || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
195
|
+
throw kvValueError('Pulse KV objects may only contain enumerable data properties.', `${path}.${key}`, value);
|
|
196
|
+
}
|
|
197
|
+
definePortableProperty(output, key, clonePortableKvValue(descriptor.value, state, `${path}.${key}`, depth + 1));
|
|
198
|
+
}
|
|
199
|
+
return Object.freeze(output);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function normalizeKvValue(value, options = {}) {
|
|
203
|
+
const allowUndefined = options.allowUndefined === true;
|
|
204
|
+
if (value === undefined && allowUndefined) return undefined;
|
|
205
|
+
const maxBytes = positiveLimit('maxKvValueBytes', options.maxKvValueBytes, DEFAULT_MAX_KV_VALUE_BYTES);
|
|
206
|
+
const maxDepth = positiveLimit('maxKvValueDepth', options.maxKvValueDepth ?? options.maxDepth, DEFAULT_MAX_KV_VALUE_DEPTH);
|
|
207
|
+
const maxEntries = positiveLimit('maxKvValueEntries', options.maxKvValueEntries ?? options.maxEntries, DEFAULT_MAX_KV_VALUE_ENTRIES);
|
|
208
|
+
const cloned = clonePortableKvValue(value, {
|
|
209
|
+
allowUndefined,
|
|
210
|
+
maxDepth,
|
|
211
|
+
maxEntries,
|
|
212
|
+
entries: 0,
|
|
213
|
+
seen: new WeakSet()
|
|
214
|
+
}, '$', 0);
|
|
215
|
+
const serialized = JSON.stringify(cloned);
|
|
216
|
+
const bytes = byteLength(serialized);
|
|
217
|
+
if (bytes > maxBytes) {
|
|
218
|
+
throw contractError(
|
|
219
|
+
'PULSE_KV_VALUE_TOO_LARGE',
|
|
220
|
+
`Pulse KV value exceeds the ${maxBytes} byte limit.`,
|
|
221
|
+
{ bytes, maxBytes }
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return cloned;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function normalizeBindingValue(capability, name, value, options = {}) {
|
|
228
|
+
const bindingKind = String(capability || 'binding').startsWith('secret') ? 'secret' : 'config';
|
|
229
|
+
// Name is intentionally validated separately from the returned value so error detail never includes it as a value.
|
|
230
|
+
normalizeBindingName(bindingKind, name, options);
|
|
231
|
+
return normalizeBindingValueResult(bindingKind, value, options);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function normalizeBindingValueResult(kind, value, options = {}) {
|
|
235
|
+
return normalizeBindingValueInternal(kind, value, options);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function normalizeKvAcknowledgement(value) {
|
|
239
|
+
if (typeof value !== 'boolean') {
|
|
240
|
+
throw contractError(
|
|
241
|
+
'PULSE_KV_ACK_INVALID',
|
|
242
|
+
'Pulse KV put providers must return a boolean acknowledgement.',
|
|
243
|
+
{ valueType: valueType(value) }
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
return value;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function cloneKvValue(value, options = {}) {
|
|
250
|
+
return normalizeKvValue(value, {
|
|
251
|
+
allowUndefined: options.allowUndefined === true,
|
|
252
|
+
maxKvValueBytes: options.maxKvValueBytes ?? options.maxBytes,
|
|
253
|
+
maxKvValueDepth: options.maxKvValueDepth ?? options.maxDepth,
|
|
254
|
+
maxKvValueEntries: options.maxKvValueEntries ?? options.maxEntries
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function normalizeKvPutResult(value) {
|
|
259
|
+
return normalizeKvAcknowledgement(value);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
module.exports = Object.freeze({
|
|
263
|
+
DEFAULT_MAX_BINDING_NAME_BYTES,
|
|
264
|
+
DEFAULT_MAX_BINDING_VALUE_BYTES,
|
|
265
|
+
DEFAULT_MAX_KV_NAMESPACE_BYTES,
|
|
266
|
+
DEFAULT_MAX_KV_KEY_BYTES,
|
|
267
|
+
DEFAULT_MAX_KV_VALUE_BYTES,
|
|
268
|
+
DEFAULT_MAX_KV_VALUE_DEPTH,
|
|
269
|
+
DEFAULT_MAX_KV_VALUE_ENTRIES,
|
|
270
|
+
byteLength,
|
|
271
|
+
cloneKvValue,
|
|
272
|
+
normalizeBindingName,
|
|
273
|
+
normalizeBindingValue,
|
|
274
|
+
normalizeBindingValueResult,
|
|
275
|
+
normalizeKvAcknowledgement,
|
|
276
|
+
normalizeKvKey,
|
|
277
|
+
normalizeKvNamespace,
|
|
278
|
+
normalizeKvPutResult,
|
|
279
|
+
normalizeKvValue,
|
|
280
|
+
positiveLimit
|
|
281
|
+
});
|