@arnilo/prism 0.0.13 → 0.0.14
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/CHANGELOG.md +12 -0
- package/README.md +8 -2
- package/dist/artifacts.d.ts +78 -0
- package/dist/artifacts.js +24 -0
- package/dist/contracts.d.ts +10 -0
- package/dist/contracts.js +8 -0
- package/dist/conversations.d.ts +50 -0
- package/dist/conversations.js +97 -0
- package/dist/credentials.d.ts +14 -0
- package/dist/credentials.js +9 -0
- package/dist/devices.d.ts +94 -0
- package/dist/devices.js +138 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.js +6 -3
- package/dist/providers/openai-primitives.js +5 -2
- package/docs/ag-ui.md +5 -0
- package/docs/browser-automation.md +3 -0
- package/docs/conversations.md +135 -0
- package/docs/credential-storage.md +28 -1
- package/docs/credentials-and-redaction.md +2 -0
- package/docs/database-persistence.md +5 -1
- package/docs/device-adapters.md +97 -0
- package/docs/host-security.md +4 -2
- package/docs/index.md +16 -12
- package/docs/migration.md +21 -0
- package/docs/performance.md +2 -0
- package/docs/policy-and-audit.md +1 -0
- package/docs/provider-caching.md +4 -0
- package/docs/provider-packages.md +4 -1
- package/docs/providers/alibaba.md +179 -0
- package/docs/providers/ollama.md +166 -0
- package/docs/release-and-install.md +75 -5
- package/docs/review-coverage-2026-07-25-phase-9.md +256 -0
- package/docs/server.md +4 -0
- package/docs/work-artifacts-and-review.md +100 -0
- package/docs/work-connectors.md +5 -1
- package/docs/work-tools.md +3 -0
- package/docs/workflows.md +4 -0
- package/docs/working-and-semantic-memory.md +20 -5
- package/package.json +1 -1
- package/templates/init/providers.json +22 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.0.14] - 2026-07-26
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Phase 9 personal/work-agent surfaces: durable conversation service (`createConversationService`), durable artifact service with review/approval/authorized delivery (`createArtifactService`), memory consent + lifecycle (`setConsent`/`correct`/`forget`/`applyRetention`), AG-UI co-work events (`mapCoWork` + ACP parity), scoped M365/GWS OAuth connectors (`revokeOAuthCredential`, `createOAuthWorkTokenProvider`), a browser verified-state checkpoint ledger, and a deny-by-default device adapter contract (`resolveDevicePolicy`/`assertDeviceAdmit`).
|
|
8
|
+
- New optional provider packages `@arnilo/prism-provider-alibaba` (Model Studio / DashScope + Coding Plan) and `@arnilo/prism-provider-ollama` (cloud/local), both with dynamic model discovery; enrolled via `@arnilo/prism-providers`.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Versioned all **43** first-party manifests and exact internal ranges to `0.0.14` (41 → 43; only the two provider packages are new).
|
|
13
|
+
- Network-free Phase 9 evidence: `scripts/benchmark-0.0.14.mjs`.
|
|
14
|
+
|
|
3
15
|
## [0.0.13] - 2026-07-24
|
|
4
16
|
|
|
5
17
|
### Added
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ packages. Prism defines contracts, not apps.
|
|
|
17
17
|
OpenAI/OpenRouter use best-effort explicit cache hints, NeuralWatt uses
|
|
18
18
|
best-effort implicit prefix caching, and other providers have route/model-specific
|
|
19
19
|
or no cache-control support; see [docs/provider-caching.md](docs/provider-caching.md).
|
|
20
|
-
- **First-party packages**:
|
|
20
|
+
- **First-party packages**: fourteen provider adapters, two compaction strategies,
|
|
21
21
|
coding tools/security, JSON Schema validation, MCP, workflows, OpenTelemetry,
|
|
22
22
|
encrypted credentials, SQLite/PostgreSQL persistence, and manifest-only install profiles.
|
|
23
23
|
- **Tools, context, skills**: host-owned tool registry with allow/deny filtering
|
|
@@ -34,6 +34,10 @@ packages. Prism defines contracts, not apps.
|
|
|
34
34
|
- **Config, settings, security**: layered config merge, settings providers,
|
|
35
35
|
credential resolvers, trust/permission policies, and secret redaction.
|
|
36
36
|
- **CLI/RPC/server**: `prism --mode print|json|rpc`, `prism init`, optional framework-free authorized Web agent/workflow routes, and explicit MCP server exposure.
|
|
37
|
+
- **Co-work contracts (0.0.14)**: conversation thread and artifact/review types, a
|
|
38
|
+
deny-by-default device-adapter contract (`resolveDevicePolicy`/`assertDeviceAdmit`),
|
|
39
|
+
and OAuth refresh/revoke helpers; services ship in `@arnilo/prism-server` and
|
|
40
|
+
`@arnilo/prism-credentials-node`.
|
|
37
41
|
|
|
38
42
|
## Install
|
|
39
43
|
|
|
@@ -150,6 +154,8 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
|
|
|
150
154
|
| `@arnilo/prism-provider-zai` | ZAI GLM provider |
|
|
151
155
|
| `@arnilo/prism-provider-kimi` | Kimi For Coding provider |
|
|
152
156
|
| `@arnilo/prism-provider-neuralwatt` | NeuralWatt provider with implicit vLLM prefix caching |
|
|
157
|
+
| `@arnilo/prism-provider-alibaba` | Alibaba Cloud (Model Studio / DashScope + Coding Plan) provider with dynamic discovery and explicit/implicit caching |
|
|
158
|
+
| `@arnilo/prism-provider-ollama` | Ollama Cloud / local provider with dynamic discovery and implicit-only caching |
|
|
153
159
|
| `@arnilo/prism-compaction-llm` | provider-backed compaction strategy |
|
|
154
160
|
| `@arnilo/prism-compaction-observational-memory` | source-backed memory + recall tool |
|
|
155
161
|
| `@arnilo/prism-coding-agent` | bounded shell/read/write/edit tools |
|
|
@@ -163,7 +169,7 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
|
|
|
163
169
|
| `@arnilo/prism-credentials-node` | encrypted-file and keychain credentials |
|
|
164
170
|
| `@arnilo/prism-session-store-sqlite` | SQLite persistence/checkpoints/leases/owned run feedback |
|
|
165
171
|
| `@arnilo/prism-session-store-postgres` | PostgreSQL persistence/checkpoints/leases/owned run feedback |
|
|
166
|
-
| `@arnilo/prism-providers` | family: all
|
|
172
|
+
| `@arnilo/prism-providers` | family: all 11 provider adapters, including AI SDK interoperability |
|
|
167
173
|
| `@arnilo/prism-compaction` | family: both compaction strategies |
|
|
168
174
|
| `@arnilo/prism-base` | profile: core + compaction + JSON Schema validation |
|
|
169
175
|
| `@arnilo/prism-code` | profile: base + coding tools/security + MCP |
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { OwnershipScope } from "./contracts.js";
|
|
2
|
+
/**
|
|
3
|
+
* Durable artifact co-work review types (Phase 9 / 0.0.14). Core exports types only;
|
|
4
|
+
* the service + delivery-link signer live in `@arnilo/prism-server`. Prism persists bounded
|
|
5
|
+
* metadata, revisions, approvals, and delivery references — never file bodies (hosts own blobs).
|
|
6
|
+
*/
|
|
7
|
+
/** Review state of an artifact's latest revision. */
|
|
8
|
+
export type ArtifactApprovalState = "pending" | "approved" | "rejected";
|
|
9
|
+
/** A resolved decision on one revision (pending is the absence of a decision). */
|
|
10
|
+
export type ArtifactDecisionState = Exclude<ArtifactApprovalState, "pending">;
|
|
11
|
+
/** Bounded citation / data-source reference. Host resolves the body; Prism stores the ref only. */
|
|
12
|
+
export interface ArtifactCitation {
|
|
13
|
+
readonly uri: string;
|
|
14
|
+
readonly title?: string;
|
|
15
|
+
/** Data-source kind (e.g. "web", "database", "upload"); host-defined, bounded. */
|
|
16
|
+
readonly kind?: string;
|
|
17
|
+
}
|
|
18
|
+
/** One immutable revision of an artifact. `uri`/`hash` reference host-owned content. */
|
|
19
|
+
export interface ArtifactRevision {
|
|
20
|
+
/** 1-based, monotonic within the artifact. */
|
|
21
|
+
readonly version: number;
|
|
22
|
+
/** Host-owned blob reference (redacted; never a local filesystem path). */
|
|
23
|
+
readonly uri: string;
|
|
24
|
+
readonly mime: string;
|
|
25
|
+
/** Host-computed content hash for integrity compare. */
|
|
26
|
+
readonly hash: string;
|
|
27
|
+
readonly changeNote?: string;
|
|
28
|
+
/** Run that produced this revision, if any. */
|
|
29
|
+
readonly producerRunId?: string;
|
|
30
|
+
readonly citations?: readonly ArtifactCitation[];
|
|
31
|
+
/** Preview metadata only; the host renders content. */
|
|
32
|
+
readonly preview?: Readonly<Record<string, unknown>>;
|
|
33
|
+
readonly createdAt: string;
|
|
34
|
+
}
|
|
35
|
+
/** A reviewer decision on a specific revision. */
|
|
36
|
+
export interface ArtifactApproval {
|
|
37
|
+
readonly version: number;
|
|
38
|
+
readonly state: ArtifactDecisionState;
|
|
39
|
+
/** Redacted reviewer actor reference. */
|
|
40
|
+
readonly reviewer: string;
|
|
41
|
+
/** Change-request / rejection note. */
|
|
42
|
+
readonly note?: string;
|
|
43
|
+
readonly decidedAt: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Durable artifact record. Stored as a versioned checkpoint value; the checkpoint version
|
|
47
|
+
* is the CAS counter for concurrent reviewers, distinct from revision numbers.
|
|
48
|
+
*/
|
|
49
|
+
export interface ArtifactRecord extends OwnershipScope {
|
|
50
|
+
readonly id: string;
|
|
51
|
+
readonly threadId: string;
|
|
52
|
+
readonly title?: string;
|
|
53
|
+
readonly revisions: readonly ArtifactRevision[];
|
|
54
|
+
readonly approvals: readonly ArtifactApproval[];
|
|
55
|
+
/** Last approved revision; remains recoverable after a later rejection. */
|
|
56
|
+
readonly lastValidatedVersion?: number;
|
|
57
|
+
readonly createdAt: string;
|
|
58
|
+
readonly updatedAt: string;
|
|
59
|
+
}
|
|
60
|
+
/** Signed, expiring delivery authorization. Reauthorized per download; never a bearer secret. */
|
|
61
|
+
export interface ArtifactDeliveryToken extends OwnershipScope {
|
|
62
|
+
readonly artifactId: string;
|
|
63
|
+
readonly threadId: string;
|
|
64
|
+
readonly version: number;
|
|
65
|
+
readonly issuedAt: string;
|
|
66
|
+
readonly expiresAt: string;
|
|
67
|
+
}
|
|
68
|
+
/** Well-known checkpoint namespace for artifact records. */
|
|
69
|
+
export declare const ARTIFACT_CHECKPOINT_NAMESPACE = "prism.artifact";
|
|
70
|
+
export declare class ArtifactError extends Error {
|
|
71
|
+
readonly reason: string;
|
|
72
|
+
readonly code = "ERR_PRISM_ARTIFACT";
|
|
73
|
+
constructor(message: string, reason: string);
|
|
74
|
+
}
|
|
75
|
+
/** Checkpoint key for an artifact: thread-scoped so per-thread listing uses a key prefix. */
|
|
76
|
+
export declare function artifactCheckpointKey(threadId: string, artifactId: string): string;
|
|
77
|
+
/** Current review state: the decision on the latest revision, or pending when undecided. */
|
|
78
|
+
export declare function artifactApprovalState(record: ArtifactRecord): ArtifactApprovalState;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Well-known checkpoint namespace for artifact records. */
|
|
2
|
+
export const ARTIFACT_CHECKPOINT_NAMESPACE = "prism.artifact";
|
|
3
|
+
export class ArtifactError extends Error {
|
|
4
|
+
reason;
|
|
5
|
+
code = "ERR_PRISM_ARTIFACT";
|
|
6
|
+
constructor(message, reason) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.reason = reason;
|
|
9
|
+
this.name = "ArtifactError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Checkpoint key for an artifact: thread-scoped so per-thread listing uses a key prefix. */
|
|
13
|
+
export function artifactCheckpointKey(threadId, artifactId) {
|
|
14
|
+
return `${threadId}:${artifactId}`;
|
|
15
|
+
}
|
|
16
|
+
/** Current review state: the decision on the latest revision, or pending when undecided. */
|
|
17
|
+
export function artifactApprovalState(record) {
|
|
18
|
+
const latest = record.revisions[record.revisions.length - 1];
|
|
19
|
+
if (latest === undefined)
|
|
20
|
+
return "pending";
|
|
21
|
+
const decision = record.approvals.find((approval) => approval.version === latest.version);
|
|
22
|
+
return decision?.state ?? "pending";
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=artifacts.js.map
|
package/dist/contracts.d.ts
CHANGED
|
@@ -968,6 +968,8 @@ export interface OAuthProvider {
|
|
|
968
968
|
readonly id: string;
|
|
969
969
|
login(callbacks?: OAuthLoginCallbacks): Promise<OAuthCredentials> | OAuthCredentials;
|
|
970
970
|
refresh?(credentials: OAuthCredentials): Promise<OAuthCredentials> | OAuthCredentials;
|
|
971
|
+
/** Best-effort upstream revocation; the store delete is what fails closed locally. */
|
|
972
|
+
revoke?(credentials: OAuthCredentials): Promise<void> | void;
|
|
971
973
|
getCredential?(credentials: OAuthCredentials): Promise<Credential | undefined> | Credential | undefined;
|
|
972
974
|
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
973
975
|
}
|
|
@@ -1493,16 +1495,21 @@ export interface MigrationRecord {
|
|
|
1493
1495
|
}
|
|
1494
1496
|
/** Query for sessions. */
|
|
1495
1497
|
export interface SessionQuery extends PersistenceQuery, OwnershipScope {
|
|
1498
|
+
readonly id?: string;
|
|
1496
1499
|
readonly parentSessionId?: string;
|
|
1497
1500
|
readonly agentDefinitionId?: string;
|
|
1498
1501
|
readonly agentDefinitionVersion?: string;
|
|
1499
1502
|
readonly retentionPolicyId?: string;
|
|
1503
|
+
/** Match sessions whose `metadata` object contains this top-level key (e.g. conversation marker). */
|
|
1504
|
+
readonly metadataKey?: string;
|
|
1500
1505
|
readonly fromCreatedAt?: string;
|
|
1501
1506
|
readonly toCreatedAt?: string;
|
|
1502
1507
|
readonly fromUpdatedAt?: string;
|
|
1503
1508
|
readonly toUpdatedAt?: string;
|
|
1504
1509
|
readonly hasExpired?: boolean;
|
|
1505
1510
|
}
|
|
1511
|
+
/** Validate a top-level `SessionRecord.metadata` key used by `SessionQuery.metadataKey` filters. */
|
|
1512
|
+
export declare function assertSessionMetadataKey(key: string): string;
|
|
1506
1513
|
/** Query for session entries. */
|
|
1507
1514
|
export interface SessionEntryQuery extends PersistenceQuery, OwnershipScope {
|
|
1508
1515
|
readonly sessionId?: string;
|
|
@@ -1613,6 +1620,9 @@ export interface ProductionPersistenceStore {
|
|
|
1613
1620
|
queryAgentDefinitions(query: AgentDefinitionQuery): Promise<PersistencePage<AgentDefinitionRecord>>;
|
|
1614
1621
|
queryRetentionPolicies(query: RetentionPolicyQuery): Promise<PersistencePage<RetentionPolicy>>;
|
|
1615
1622
|
queryMigrations(query: MigrationQuery): Promise<PersistencePage<MigrationRecord>>;
|
|
1623
|
+
/** Optional session-record write capability (conversation threads, host-managed sessions).
|
|
1624
|
+
* Upserts by id; ownership columns are set on create, `metadata`/`updatedAt` on update. */
|
|
1625
|
+
appendSession?(record: SessionRecord): Promise<void>;
|
|
1616
1626
|
/** DB-friendly branch read (mirrors `SessionStore.readBranchPath`): one ancestor-chain
|
|
1617
1627
|
* query instead of `queryEntries({ sessionId })` + in-memory walk. Optional. */
|
|
1618
1628
|
readBranchPath?(query: SessionBranchRead): Promise<PersistencePage<SessionEntry>>;
|
package/dist/contracts.js
CHANGED
|
@@ -128,4 +128,12 @@ export class SessionAppendConflictError extends Error {
|
|
|
128
128
|
export function isSessionAppendConflict(error) {
|
|
129
129
|
return error instanceof Error && error.code === SESSION_APPEND_CONFLICT_CODE;
|
|
130
130
|
}
|
|
131
|
+
const SESSION_METADATA_KEY_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
|
|
132
|
+
/** Validate a top-level `SessionRecord.metadata` key used by `SessionQuery.metadataKey` filters. */
|
|
133
|
+
export function assertSessionMetadataKey(key) {
|
|
134
|
+
if (typeof key !== "string" || !SESSION_METADATA_KEY_PATTERN.test(key)) {
|
|
135
|
+
throw new RangeError("metadataKey must match /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/");
|
|
136
|
+
}
|
|
137
|
+
return key;
|
|
138
|
+
}
|
|
131
139
|
//# sourceMappingURL=contracts.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { OwnershipScope, SessionRecord } from "./contracts.js";
|
|
2
|
+
/** Conversation thread lifecycle state. Archive is soft; deletion goes through persistence lifecycle. */
|
|
3
|
+
export type ConversationThreadState = "active" | "archived";
|
|
4
|
+
/** Well-known `SessionRecord.metadata` key marking conversation threads. */
|
|
5
|
+
export declare const CONVERSATION_METADATA_KEY = "prismConversation";
|
|
6
|
+
export declare const DEFAULT_MAX_CONVERSATION_CURSOR_BYTES: number;
|
|
7
|
+
export declare const HARD_MAX_CONVERSATION_CURSOR_BYTES: number;
|
|
8
|
+
export interface ConversationBranchRef {
|
|
9
|
+
readonly leafId: string;
|
|
10
|
+
readonly createdAt: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Durable user-scoped conversation thread. A thread is an ownership-scoped session
|
|
14
|
+
* branch plus metadata; content lives in session entries and the redacted event ledger.
|
|
15
|
+
*/
|
|
16
|
+
export interface ConversationThread extends OwnershipScope {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly title?: string;
|
|
19
|
+
readonly state: ConversationThreadState;
|
|
20
|
+
readonly createdAt: string;
|
|
21
|
+
readonly updatedAt: string;
|
|
22
|
+
/** Branch leaves recorded by the conversation service; the entry tree remains the content source of truth. */
|
|
23
|
+
readonly branches: readonly ConversationBranchRef[];
|
|
24
|
+
/** Host-supplied create metadata; never credentials or raw transcripts. */
|
|
25
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
26
|
+
}
|
|
27
|
+
/** Opaque thread-bound replay cursor; prevents replaying a cursor minted for another thread. */
|
|
28
|
+
export interface ConversationReplayCursor {
|
|
29
|
+
readonly v: 1;
|
|
30
|
+
readonly threadId: string;
|
|
31
|
+
/** Opaque store keyset cursor; undefined means the start of the thread. */
|
|
32
|
+
readonly cursor?: string;
|
|
33
|
+
}
|
|
34
|
+
export declare class ConversationError extends Error {
|
|
35
|
+
readonly reason: string;
|
|
36
|
+
readonly code = "ERR_PRISM_CONVERSATION";
|
|
37
|
+
constructor(message: string, reason: string);
|
|
38
|
+
}
|
|
39
|
+
export declare function encodeConversationReplayCursor(cursor: ConversationReplayCursor): string;
|
|
40
|
+
export declare function decodeConversationReplayCursor(encoded: string, expectedThreadId: string, maxBytes?: number): ConversationReplayCursor;
|
|
41
|
+
/** Project a persisted session record into a conversation thread. Undefined for non-conversation sessions. */
|
|
42
|
+
export declare function conversationThreadFromRecord(record: SessionRecord): ConversationThread | undefined;
|
|
43
|
+
/** Serialize conversation marker metadata for `SessionRecord.metadata`. */
|
|
44
|
+
export declare function conversationMarkerMetadata(marker: {
|
|
45
|
+
readonly title?: string;
|
|
46
|
+
readonly state: ConversationThreadState;
|
|
47
|
+
readonly branches?: readonly ConversationBranchRef[];
|
|
48
|
+
readonly requestId?: string;
|
|
49
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
50
|
+
}): Readonly<Record<string, unknown>>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** Well-known `SessionRecord.metadata` key marking conversation threads. */
|
|
2
|
+
export const CONVERSATION_METADATA_KEY = "prismConversation";
|
|
3
|
+
export const DEFAULT_MAX_CONVERSATION_CURSOR_BYTES = 4 * 1024;
|
|
4
|
+
export const HARD_MAX_CONVERSATION_CURSOR_BYTES = 16 * 1024;
|
|
5
|
+
export class ConversationError extends Error {
|
|
6
|
+
reason;
|
|
7
|
+
code = "ERR_PRISM_CONVERSATION";
|
|
8
|
+
constructor(message, reason) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.reason = reason;
|
|
11
|
+
this.name = "ConversationError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function encodeConversationReplayCursor(cursor) {
|
|
15
|
+
return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
|
|
16
|
+
}
|
|
17
|
+
export function decodeConversationReplayCursor(encoded, expectedThreadId, maxBytes = DEFAULT_MAX_CONVERSATION_CURSOR_BYTES) {
|
|
18
|
+
if (typeof encoded !== "string" || encoded.length === 0) {
|
|
19
|
+
throw new ConversationError("Replay cursor is required", "invalid_cursor");
|
|
20
|
+
}
|
|
21
|
+
if (Buffer.byteLength(encoded, "utf8") > maxBytes) {
|
|
22
|
+
throw new ConversationError("Replay cursor exceeds byte limit", "cursor_too_large");
|
|
23
|
+
}
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new ConversationError("Replay cursor is invalid", "invalid_cursor");
|
|
30
|
+
}
|
|
31
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
32
|
+
throw new ConversationError("Replay cursor is invalid", "invalid_cursor");
|
|
33
|
+
}
|
|
34
|
+
const cursor = parsed;
|
|
35
|
+
if (cursor.v !== 1 || typeof cursor.threadId !== "string" || cursor.threadId.length === 0) {
|
|
36
|
+
throw new ConversationError("Replay cursor is invalid", "invalid_cursor");
|
|
37
|
+
}
|
|
38
|
+
if (cursor.threadId !== expectedThreadId) {
|
|
39
|
+
throw new ConversationError("Replay cursor belongs to another thread", "cursor_thread_mismatch");
|
|
40
|
+
}
|
|
41
|
+
if (cursor.cursor !== undefined && (typeof cursor.cursor !== "string" || cursor.cursor.length === 0)) {
|
|
42
|
+
throw new ConversationError("Replay cursor is invalid", "invalid_cursor");
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
v: 1,
|
|
46
|
+
threadId: cursor.threadId,
|
|
47
|
+
...(cursor.cursor === undefined ? {} : { cursor: cursor.cursor }),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Project a persisted session record into a conversation thread. Undefined for non-conversation sessions. */
|
|
51
|
+
export function conversationThreadFromRecord(record) {
|
|
52
|
+
const marker = record.metadata?.[CONVERSATION_METADATA_KEY];
|
|
53
|
+
if (!marker || typeof marker !== "object" || Array.isArray(marker))
|
|
54
|
+
return undefined;
|
|
55
|
+
const value = marker;
|
|
56
|
+
const branches = [];
|
|
57
|
+
if (Array.isArray(value.branches)) {
|
|
58
|
+
for (const item of value.branches) {
|
|
59
|
+
if (item && typeof item === "object" &&
|
|
60
|
+
typeof item.leafId === "string" &&
|
|
61
|
+
typeof item.createdAt === "string") {
|
|
62
|
+
branches.push({
|
|
63
|
+
leafId: item.leafId,
|
|
64
|
+
createdAt: item.createdAt,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const metadata = value.metadata && typeof value.metadata === "object" && !Array.isArray(value.metadata)
|
|
70
|
+
? value.metadata
|
|
71
|
+
: undefined;
|
|
72
|
+
return {
|
|
73
|
+
id: record.id,
|
|
74
|
+
...(record.tenantId !== undefined ? { tenantId: record.tenantId } : {}),
|
|
75
|
+
...(record.accountId !== undefined ? { accountId: record.accountId } : {}),
|
|
76
|
+
...(record.userId !== undefined ? { userId: record.userId } : {}),
|
|
77
|
+
...(typeof value.title === "string" && value.title.length > 0 ? { title: value.title } : {}),
|
|
78
|
+
state: value.state === "archived" ? "archived" : "active",
|
|
79
|
+
createdAt: record.createdAt,
|
|
80
|
+
updatedAt: record.updatedAt,
|
|
81
|
+
branches: Object.freeze(branches),
|
|
82
|
+
...(metadata === undefined ? {} : { metadata }),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Serialize conversation marker metadata for `SessionRecord.metadata`. */
|
|
86
|
+
export function conversationMarkerMetadata(marker) {
|
|
87
|
+
return {
|
|
88
|
+
[CONVERSATION_METADATA_KEY]: {
|
|
89
|
+
...(marker.title === undefined ? {} : { title: marker.title }),
|
|
90
|
+
state: marker.state,
|
|
91
|
+
...(marker.branches === undefined || marker.branches.length === 0 ? {} : { branches: marker.branches }),
|
|
92
|
+
...(marker.requestId === undefined ? {} : { requestId: marker.requestId }),
|
|
93
|
+
...(marker.metadata === undefined ? {} : { metadata: marker.metadata }),
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=conversations.js.map
|
package/dist/credentials.d.ts
CHANGED
|
@@ -19,4 +19,18 @@ export declare function refreshOAuthCredential(options: {
|
|
|
19
19
|
readonly credentials: OAuthCredentials;
|
|
20
20
|
readonly store?: OAuthCredentialStore;
|
|
21
21
|
}): Promise<OAuthCredentials>;
|
|
22
|
+
/** A credential store that can also remove entries (revocation fails closed on the delete). */
|
|
23
|
+
export interface RevocableOAuthCredentialStore extends OAuthCredentialStore {
|
|
24
|
+
delete(provider: string, accountId?: string): Promise<boolean> | boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Revokes an OAuth credential: best-effort upstream revocation, then a mandatory local
|
|
28
|
+
* store delete so subsequent connector calls fail closed. The local delete is the trust
|
|
29
|
+
* boundary — an upstream revoke failure never leaves a stored token usable.
|
|
30
|
+
*/
|
|
31
|
+
export declare function revokeOAuthCredential(options: {
|
|
32
|
+
readonly provider: OAuthProvider;
|
|
33
|
+
readonly credentials: OAuthCredentials;
|
|
34
|
+
readonly store?: RevocableOAuthCredentialStore;
|
|
35
|
+
}): Promise<void>;
|
|
22
36
|
export declare function resolveCredentialValue(source: CredentialValueSource | undefined, request: CredentialRequest): Promise<string | undefined>;
|
package/dist/credentials.js
CHANGED
|
@@ -48,6 +48,15 @@ export async function refreshOAuthCredential(options) {
|
|
|
48
48
|
await options.store?.set(options.provider.id, refreshed);
|
|
49
49
|
return refreshed;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Revokes an OAuth credential: best-effort upstream revocation, then a mandatory local
|
|
53
|
+
* store delete so subsequent connector calls fail closed. The local delete is the trust
|
|
54
|
+
* boundary — an upstream revoke failure never leaves a stored token usable.
|
|
55
|
+
*/
|
|
56
|
+
export async function revokeOAuthCredential(options) {
|
|
57
|
+
await options.provider.revoke?.(options.credentials);
|
|
58
|
+
await options.store?.delete(options.provider.id, options.credentials.accountId);
|
|
59
|
+
}
|
|
51
60
|
function credentialMapKey(name, provider) {
|
|
52
61
|
return provider ? `${provider}:${name}` : name;
|
|
53
62
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device adapter contract (realtime voice / desktop OS control) — 0.0.14.
|
|
3
|
+
*
|
|
4
|
+
* Contracts + deny-by-default policy ONLY. No vendor voice or desktop-control
|
|
5
|
+
* implementation ships in 0.0.14 (demand-gated to 0.1.x). This module composes
|
|
6
|
+
* over the existing `PermissionPolicy` / `RunLimits` / redactor seams; it adds
|
|
7
|
+
* no second approval runtime and no device framework. Hosts implement
|
|
8
|
+
* `DeviceAdapter`, resolve a policy, and admit sessions through the fail-closed
|
|
9
|
+
* gate below. Conformance fixtures (denial/approval/stream-bounds/redaction)
|
|
10
|
+
* live in `runDevicePolicyConformance` for future vendor adapters to run.
|
|
11
|
+
*/
|
|
12
|
+
import type { RunLimits } from "./contracts.js";
|
|
13
|
+
import { type SecretRedactor } from "./redaction.js";
|
|
14
|
+
/** Audio / screenshot / stream chunk: 1 MiB default / 8 MiB hard. */
|
|
15
|
+
export declare const DEFAULT_DEVICE_MAX_CHUNK_BYTES: number;
|
|
16
|
+
export declare const HARD_DEVICE_MAX_CHUNK_BYTES: number;
|
|
17
|
+
/** Concurrent device sessions per identity: 1 default / 4 hard. */
|
|
18
|
+
export declare const DEFAULT_DEVICE_MAX_CONCURRENT_SESSIONS = 1;
|
|
19
|
+
export declare const HARD_DEVICE_MAX_CONCURRENT_SESSIONS = 4;
|
|
20
|
+
export type DeviceKind = "voice" | "desktop-control";
|
|
21
|
+
export type DevicePolicyErrorCode = "ERR_PRISM_DEVICE_INPUT" | "ERR_PRISM_DEVICE_DISABLED" | "ERR_PRISM_DEVICE_APPROVAL" | "ERR_PRISM_DEVICE_SESSIONS" | "ERR_PRISM_DEVICE_CHUNK" | "ERR_PRISM_DEVICE_RUN_LIMITS";
|
|
22
|
+
export declare class DevicePolicyError extends Error {
|
|
23
|
+
readonly code: DevicePolicyErrorCode;
|
|
24
|
+
constructor(code: DevicePolicyErrorCode, message: string);
|
|
25
|
+
}
|
|
26
|
+
export interface DeviceStreamLimits {
|
|
27
|
+
readonly maxChunkBytes?: number;
|
|
28
|
+
readonly maxConcurrentSessions?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Host-declared device adapter. `enabled` is deny-by-default: a device is
|
|
32
|
+
* admitted only when the host explicitly sets it `true` AND supplies a sandbox
|
|
33
|
+
* AND (when `requireApproval`) explicit per-side-effect approval.
|
|
34
|
+
*/
|
|
35
|
+
export interface DeviceAdapter {
|
|
36
|
+
readonly kind: DeviceKind;
|
|
37
|
+
readonly enabled: boolean;
|
|
38
|
+
readonly requireApproval: boolean;
|
|
39
|
+
readonly limits?: DeviceStreamLimits;
|
|
40
|
+
/** Host-owned sandbox identifier; admission fails closed without it. */
|
|
41
|
+
readonly sandbox?: string;
|
|
42
|
+
/** Host-owned network/egress policy identifier. */
|
|
43
|
+
readonly network?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface ResolvedDevicePolicy {
|
|
46
|
+
readonly kind: DeviceKind;
|
|
47
|
+
readonly enabled: boolean;
|
|
48
|
+
readonly requireApproval: boolean;
|
|
49
|
+
readonly maxChunkBytes: number;
|
|
50
|
+
readonly maxConcurrentSessions: number;
|
|
51
|
+
readonly sandbox?: string;
|
|
52
|
+
readonly network?: string;
|
|
53
|
+
/** Shared run accounting the device session must consume. */
|
|
54
|
+
readonly runLimits?: RunLimits;
|
|
55
|
+
}
|
|
56
|
+
export interface DevicePolicyOptions {
|
|
57
|
+
readonly maxChunkBytes?: number;
|
|
58
|
+
readonly maxConcurrentSessions?: number;
|
|
59
|
+
readonly runLimits?: RunLimits;
|
|
60
|
+
}
|
|
61
|
+
export declare function resolveDevicePolicy(adapter: DeviceAdapter, options?: DevicePolicyOptions): ResolvedDevicePolicy;
|
|
62
|
+
export interface DeviceAdmitRequest {
|
|
63
|
+
/** Explicit host approval for this device side effect. */
|
|
64
|
+
readonly approved: boolean;
|
|
65
|
+
/** Currently active device sessions for this identity. */
|
|
66
|
+
readonly activeSessions: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Fail-closed admission gate. Denies unless the device is explicitly enabled,
|
|
70
|
+
* sandboxed, approved (when required), under the concurrent-session budget, and
|
|
71
|
+
* bound to shared run accounting. Side effects never replay after reconnect:
|
|
72
|
+
* hosts must re-admit on every resume.
|
|
73
|
+
*/
|
|
74
|
+
export declare function assertDeviceAdmit(policy: ResolvedDevicePolicy, request: DeviceAdmitRequest): void;
|
|
75
|
+
export interface DeviceChunkResult {
|
|
76
|
+
readonly accepted: boolean;
|
|
77
|
+
readonly bytes: number;
|
|
78
|
+
/** Present when the chunk exceeded the stream bound and was dropped. */
|
|
79
|
+
readonly marker?: "dropped_oversize";
|
|
80
|
+
}
|
|
81
|
+
/** Stream bound: oversize audio/screenshot/stream chunks are dropped with a marker, never forwarded. */
|
|
82
|
+
export declare function acceptDeviceChunk(policy: ResolvedDevicePolicy, bytes: number): DeviceChunkResult;
|
|
83
|
+
/** Telemetry must be metadata-safe: apply the host redactor before any emit/persist. */
|
|
84
|
+
export declare function redactDeviceTelemetry<T>(redactor: SecretRedactor | undefined, telemetry: T): T;
|
|
85
|
+
export interface DeviceConformanceResult {
|
|
86
|
+
readonly passed: readonly string[];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Conformance pair for future voice / desktop-control adapters. Runs the
|
|
90
|
+
* deny-by-default fixtures (denial, approval, stream bounds, session budget,
|
|
91
|
+
* run accounting, redaction) against a resolved policy and throws on any
|
|
92
|
+
* regression. Tested in 0.0.14 via fixtures only.
|
|
93
|
+
*/
|
|
94
|
+
export declare function runDevicePolicyConformance(adapter: DeviceAdapter, options?: DevicePolicyOptions): DeviceConformanceResult;
|
package/dist/devices.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { redactSecrets } from "./redaction.js";
|
|
2
|
+
/** Audio / screenshot / stream chunk: 1 MiB default / 8 MiB hard. */
|
|
3
|
+
export const DEFAULT_DEVICE_MAX_CHUNK_BYTES = 1 * 1024 * 1024;
|
|
4
|
+
export const HARD_DEVICE_MAX_CHUNK_BYTES = 8 * 1024 * 1024;
|
|
5
|
+
/** Concurrent device sessions per identity: 1 default / 4 hard. */
|
|
6
|
+
export const DEFAULT_DEVICE_MAX_CONCURRENT_SESSIONS = 1;
|
|
7
|
+
export const HARD_DEVICE_MAX_CONCURRENT_SESSIONS = 4;
|
|
8
|
+
export class DevicePolicyError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
constructor(code, message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.name = "DevicePolicyError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function validateCap(name, value, def, hard) {
|
|
17
|
+
const resolved = value ?? def;
|
|
18
|
+
if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > hard) {
|
|
19
|
+
throw new DevicePolicyError("ERR_PRISM_DEVICE_INPUT", `${name} must be a positive safe integer at most ${hard}`);
|
|
20
|
+
}
|
|
21
|
+
return resolved;
|
|
22
|
+
}
|
|
23
|
+
export function resolveDevicePolicy(adapter, options = {}) {
|
|
24
|
+
if (!adapter || (adapter.kind !== "voice" && adapter.kind !== "desktop-control")) {
|
|
25
|
+
throw new DevicePolicyError("ERR_PRISM_DEVICE_INPUT", "device kind must be 'voice' or 'desktop-control'");
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
kind: adapter.kind,
|
|
29
|
+
// Deny-by-default: anything but an explicit `true` resolves to disabled.
|
|
30
|
+
enabled: adapter.enabled === true,
|
|
31
|
+
// Approval is required unless the host explicitly opts out (it should not).
|
|
32
|
+
requireApproval: adapter.requireApproval !== false,
|
|
33
|
+
maxChunkBytes: validateCap("maxChunkBytes", options.maxChunkBytes ?? adapter.limits?.maxChunkBytes, DEFAULT_DEVICE_MAX_CHUNK_BYTES, HARD_DEVICE_MAX_CHUNK_BYTES),
|
|
34
|
+
maxConcurrentSessions: validateCap("maxConcurrentSessions", options.maxConcurrentSessions ?? adapter.limits?.maxConcurrentSessions, DEFAULT_DEVICE_MAX_CONCURRENT_SESSIONS, HARD_DEVICE_MAX_CONCURRENT_SESSIONS),
|
|
35
|
+
...(adapter.sandbox ? { sandbox: adapter.sandbox } : {}),
|
|
36
|
+
...(adapter.network ? { network: adapter.network } : {}),
|
|
37
|
+
...(options.runLimits ? { runLimits: options.runLimits } : {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Fail-closed admission gate. Denies unless the device is explicitly enabled,
|
|
42
|
+
* sandboxed, approved (when required), under the concurrent-session budget, and
|
|
43
|
+
* bound to shared run accounting. Side effects never replay after reconnect:
|
|
44
|
+
* hosts must re-admit on every resume.
|
|
45
|
+
*/
|
|
46
|
+
export function assertDeviceAdmit(policy, request) {
|
|
47
|
+
if (!policy.enabled) {
|
|
48
|
+
throw new DevicePolicyError("ERR_PRISM_DEVICE_DISABLED", `${policy.kind} device is disabled by default`);
|
|
49
|
+
}
|
|
50
|
+
if (!policy.sandbox) {
|
|
51
|
+
throw new DevicePolicyError("ERR_PRISM_DEVICE_DISABLED", `${policy.kind} device requires an explicit sandbox`);
|
|
52
|
+
}
|
|
53
|
+
if (policy.requireApproval && !request.approved) {
|
|
54
|
+
throw new DevicePolicyError("ERR_PRISM_DEVICE_APPROVAL", `${policy.kind} device side effect requires approval`);
|
|
55
|
+
}
|
|
56
|
+
if (!Number.isSafeInteger(request.activeSessions) || request.activeSessions < 0) {
|
|
57
|
+
throw new DevicePolicyError("ERR_PRISM_DEVICE_INPUT", "activeSessions must be a non-negative safe integer");
|
|
58
|
+
}
|
|
59
|
+
if (request.activeSessions >= policy.maxConcurrentSessions) {
|
|
60
|
+
throw new DevicePolicyError("ERR_PRISM_DEVICE_SESSIONS", `concurrent ${policy.kind} sessions capped at ${policy.maxConcurrentSessions}`);
|
|
61
|
+
}
|
|
62
|
+
if (!policy.runLimits) {
|
|
63
|
+
throw new DevicePolicyError("ERR_PRISM_DEVICE_RUN_LIMITS", `${policy.kind} device must consume shared RunLimits`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Stream bound: oversize audio/screenshot/stream chunks are dropped with a marker, never forwarded. */
|
|
67
|
+
export function acceptDeviceChunk(policy, bytes) {
|
|
68
|
+
if (!Number.isSafeInteger(bytes) || bytes < 0) {
|
|
69
|
+
throw new DevicePolicyError("ERR_PRISM_DEVICE_CHUNK", "chunk bytes must be a non-negative safe integer");
|
|
70
|
+
}
|
|
71
|
+
if (bytes > policy.maxChunkBytes) {
|
|
72
|
+
return { accepted: false, bytes, marker: "dropped_oversize" };
|
|
73
|
+
}
|
|
74
|
+
return { accepted: true, bytes };
|
|
75
|
+
}
|
|
76
|
+
/** Telemetry must be metadata-safe: apply the host redactor before any emit/persist. */
|
|
77
|
+
export function redactDeviceTelemetry(redactor, telemetry) {
|
|
78
|
+
return redactor ? redactor.redact(telemetry) : telemetry;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Conformance pair for future voice / desktop-control adapters. Runs the
|
|
82
|
+
* deny-by-default fixtures (denial, approval, stream bounds, session budget,
|
|
83
|
+
* run accounting, redaction) against a resolved policy and throws on any
|
|
84
|
+
* regression. Tested in 0.0.14 via fixtures only.
|
|
85
|
+
*/
|
|
86
|
+
export function runDevicePolicyConformance(adapter, options = {}) {
|
|
87
|
+
const runLimits = options.runLimits ?? { maxTurns: 1 };
|
|
88
|
+
const expectThrow = (code, fn) => {
|
|
89
|
+
let threw = false;
|
|
90
|
+
try {
|
|
91
|
+
fn();
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
threw = error instanceof DevicePolicyError && error.code === code;
|
|
95
|
+
}
|
|
96
|
+
if (!threw)
|
|
97
|
+
throw new DevicePolicyError(code, `conformance: expected ${code}`);
|
|
98
|
+
};
|
|
99
|
+
const passed = [];
|
|
100
|
+
// 1. Denial by default: a disabled adapter never admits.
|
|
101
|
+
const disabled = resolveDevicePolicy({ ...adapter, enabled: false }, { ...options, runLimits });
|
|
102
|
+
expectThrow("ERR_PRISM_DEVICE_DISABLED", () => assertDeviceAdmit(disabled, { approved: true, activeSessions: 0 }));
|
|
103
|
+
passed.push("denial-by-default");
|
|
104
|
+
// 2. Approval gate: enabled+sandboxed but unapproved denies; approved admits.
|
|
105
|
+
const enabled = resolveDevicePolicy({ ...adapter, enabled: true, requireApproval: true, sandbox: adapter.sandbox ?? "sandbox" }, { ...options, runLimits });
|
|
106
|
+
expectThrow("ERR_PRISM_DEVICE_APPROVAL", () => assertDeviceAdmit(enabled, { approved: false, activeSessions: 0 }));
|
|
107
|
+
assertDeviceAdmit(enabled, { approved: true, activeSessions: 0 });
|
|
108
|
+
passed.push("approval-gate");
|
|
109
|
+
// 3. Session budget: at/over the concurrent cap denies.
|
|
110
|
+
expectThrow("ERR_PRISM_DEVICE_SESSIONS", () => assertDeviceAdmit(enabled, { approved: true, activeSessions: enabled.maxConcurrentSessions }));
|
|
111
|
+
passed.push("session-budget");
|
|
112
|
+
// 4. Run accounting: no shared RunLimits denies.
|
|
113
|
+
const unaccounted = resolveDevicePolicy({ ...adapter, enabled: true, sandbox: "sandbox" }, { ...options, runLimits: undefined });
|
|
114
|
+
expectThrow("ERR_PRISM_DEVICE_RUN_LIMITS", () => assertDeviceAdmit(unaccounted, { approved: true, activeSessions: 0 }));
|
|
115
|
+
passed.push("run-accounting");
|
|
116
|
+
// 5. Stream bounds: oversize chunk dropped with marker; in-bound accepted.
|
|
117
|
+
const over = acceptDeviceChunk(enabled, enabled.maxChunkBytes + 1);
|
|
118
|
+
if (over.accepted || over.marker !== "dropped_oversize") {
|
|
119
|
+
throw new DevicePolicyError("ERR_PRISM_DEVICE_CHUNK", "conformance: oversize chunk must be dropped");
|
|
120
|
+
}
|
|
121
|
+
if (!acceptDeviceChunk(enabled, enabled.maxChunkBytes).accepted) {
|
|
122
|
+
throw new DevicePolicyError("ERR_PRISM_DEVICE_CHUNK", "conformance: in-bound chunk must accept");
|
|
123
|
+
}
|
|
124
|
+
passed.push("stream-bounds");
|
|
125
|
+
// 6. Redaction: telemetry passes through the host redactor (secrets stripped), passthrough when absent.
|
|
126
|
+
const secret = "super-secret-device-token";
|
|
127
|
+
const redactor = { redact: (value) => redactSecrets(value, [secret]) };
|
|
128
|
+
const redacted = redactDeviceTelemetry(redactor, { note: `leak ${secret}` });
|
|
129
|
+
if (redacted.note.includes(secret)) {
|
|
130
|
+
throw new DevicePolicyError("ERR_PRISM_DEVICE_INPUT", "conformance: telemetry secret must be redacted");
|
|
131
|
+
}
|
|
132
|
+
if (redactDeviceTelemetry(undefined, { note: "ok" }).note !== "ok") {
|
|
133
|
+
throw new DevicePolicyError("ERR_PRISM_DEVICE_INPUT", "conformance: telemetry must pass through without a redactor");
|
|
134
|
+
}
|
|
135
|
+
passed.push("redaction");
|
|
136
|
+
return { passed };
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=devices.js.map
|