@pylonsync/sdk 0.3.196 → 0.3.198
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/package.json +1 -1
- package/src/index.ts +179 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -96,6 +96,29 @@ export interface FieldDefinition {
|
|
|
96
96
|
* Server code is trusted to enforce its own invariants.
|
|
97
97
|
*/
|
|
98
98
|
readonly?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* When true, the field is AEAD-encrypted at rest. The framework
|
|
101
|
+
* encrypts the value before writing to SQLite/Postgres and
|
|
102
|
+
* decrypts on read. Cipher: ChaCha20-Poly1305. Key:
|
|
103
|
+
* `PYLON_ENCRYPTION_KEY` env (32 bytes, hex or base64).
|
|
104
|
+
*
|
|
105
|
+
* Use for PII / secrets that must survive a DB-file leak: API
|
|
106
|
+
* keys stored on rows, social security numbers, OAuth tokens.
|
|
107
|
+
* Plaintext only exists inside the Pylon process.
|
|
108
|
+
*
|
|
109
|
+
* Restrictions:
|
|
110
|
+
* - Encrypted fields are NOT queryable. `ctx.db.lookup` /
|
|
111
|
+
* `WHERE encryptedField = 'x'` always returns nothing because
|
|
112
|
+
* each write produces fresh ciphertext.
|
|
113
|
+
* - Cannot combine with `unique: true`.
|
|
114
|
+
* - Valid only on `string`, `richtext`, and JSON-shaped fields.
|
|
115
|
+
*
|
|
116
|
+
* Decryption pre-pass means rows written BEFORE the field was
|
|
117
|
+
* annotated `encrypted: true` continue to read fine (passes
|
|
118
|
+
* through as plaintext). Next write through the mutation
|
|
119
|
+
* pipeline upgrades them to ciphertext.
|
|
120
|
+
*/
|
|
121
|
+
encrypted?: boolean;
|
|
99
122
|
}
|
|
100
123
|
|
|
101
124
|
interface FieldBuilder {
|
|
@@ -131,6 +154,16 @@ interface FieldBuilder {
|
|
|
131
154
|
* closes the IDOR-via-update-payload class.
|
|
132
155
|
*/
|
|
133
156
|
readonly(): FieldBuilder;
|
|
157
|
+
/**
|
|
158
|
+
* Mark the field as AEAD-encrypted at rest. See
|
|
159
|
+
* [`FieldDefinition.encrypted`] for the full semantics +
|
|
160
|
+
* restrictions.
|
|
161
|
+
*
|
|
162
|
+
* Example: `apiKey: field.string().serverOnly().encrypted()`
|
|
163
|
+
* keeps the key out of HTTP responses AND encrypts the bytes
|
|
164
|
+
* sitting in SQLite.
|
|
165
|
+
*/
|
|
166
|
+
encrypted(): FieldBuilder;
|
|
134
167
|
}
|
|
135
168
|
|
|
136
169
|
function createFieldBuilder(type: FieldType): FieldBuilder {
|
|
@@ -155,6 +188,9 @@ function buildField(def: FieldDefinition): FieldBuilder {
|
|
|
155
188
|
readonly() {
|
|
156
189
|
return buildField({ ...def, readonly: true });
|
|
157
190
|
},
|
|
191
|
+
encrypted() {
|
|
192
|
+
return buildField({ ...def, encrypted: true });
|
|
193
|
+
},
|
|
158
194
|
};
|
|
159
195
|
}
|
|
160
196
|
|
|
@@ -405,6 +441,9 @@ export interface ManifestField {
|
|
|
405
441
|
* reject out-of-set inserts. Plain `field.string()` doesn't
|
|
406
442
|
* carry this; only `field.enum()`. */
|
|
407
443
|
enumValues?: readonly string[];
|
|
444
|
+
/** Set when the field is `field.X().encrypted()` — AEAD-encrypted
|
|
445
|
+
* at rest. See [`FieldDefinition.encrypted`]. */
|
|
446
|
+
encrypted?: boolean;
|
|
408
447
|
}
|
|
409
448
|
|
|
410
449
|
export interface ManifestIndex {
|
|
@@ -487,6 +526,11 @@ export interface AppManifest {
|
|
|
487
526
|
actions: ManifestAction[];
|
|
488
527
|
policies: ManifestPolicy[];
|
|
489
528
|
auth?: ManifestAuthConfig;
|
|
529
|
+
/** App-level LLM provider config. Optional — env wins when set. */
|
|
530
|
+
llm?: ManifestLlmConfig;
|
|
531
|
+
/** Declared OAuth integrations. Auto-creates the `_Connection`
|
|
532
|
+
* entity at runtime boot. */
|
|
533
|
+
connections?: ManifestConnection[];
|
|
490
534
|
}
|
|
491
535
|
|
|
492
536
|
export function entitiesToManifest(
|
|
@@ -514,6 +558,9 @@ export function entitiesToManifest(
|
|
|
514
558
|
if (fb._def.readonly) {
|
|
515
559
|
f.readonly = true;
|
|
516
560
|
}
|
|
561
|
+
if (fb._def.encrypted) {
|
|
562
|
+
f.encrypted = true;
|
|
563
|
+
}
|
|
517
564
|
// `default` + `enumValues` are surfaced on the fluent
|
|
518
565
|
// FieldBuilder via the v0.4 SDK. Read off the private
|
|
519
566
|
// backing slot so both APIs serialize identically — apps
|
|
@@ -735,6 +782,127 @@ export type AuthConfig = {
|
|
|
735
782
|
trustedOrigins?: string[];
|
|
736
783
|
};
|
|
737
784
|
|
|
785
|
+
// ---------------------------------------------------------------------------
|
|
786
|
+
// LLM provider configuration
|
|
787
|
+
// ---------------------------------------------------------------------------
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Developer-facing camelCase config consumed by the `llm({...})`
|
|
791
|
+
* factory. All fields optional; environment variables
|
|
792
|
+
* (`PYLON_LLM_PROVIDER`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`,
|
|
793
|
+
* `PYLON_LLM_MODEL`) take precedence so operators can override per
|
|
794
|
+
* deploy without redeploying the bundle.
|
|
795
|
+
*/
|
|
796
|
+
export type LlmConfig = {
|
|
797
|
+
/** Provider name. Default: env detection. */
|
|
798
|
+
provider?: "anthropic" | "openai";
|
|
799
|
+
/** Default model when the caller doesn't pass `model`. */
|
|
800
|
+
defaultModel?: string;
|
|
801
|
+
/**
|
|
802
|
+
* Allowlist of models callers may request via the `model` field.
|
|
803
|
+
* Empty = no extra allowance beyond what `PYLON_AI_MODELS_ALLOWED`
|
|
804
|
+
* env provides. Non-admin callers can't request models outside
|
|
805
|
+
* this list.
|
|
806
|
+
*/
|
|
807
|
+
allowedModels?: string[];
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
export type ManifestLlmConfig = {
|
|
811
|
+
provider?: "anthropic" | "openai";
|
|
812
|
+
default_model?: string;
|
|
813
|
+
allowed_models?: string[];
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
// ---------------------------------------------------------------------------
|
|
817
|
+
// Connection (per-user OAuth integrations)
|
|
818
|
+
// ---------------------------------------------------------------------------
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Developer-facing config for `defineConnection({...})`. Each entry
|
|
822
|
+
* adds a `ctx.connections.<name>` surface to mutation + action ctx
|
|
823
|
+
* (server-side OAuth tokens, never visible to the browser).
|
|
824
|
+
*
|
|
825
|
+
* `provider` selects the OAuth client wire shape from pylon-auth's
|
|
826
|
+
* built-in list (`google`, `github`, `slack`, `microsoft`, etc.).
|
|
827
|
+
* `name` is the app-facing key — different connections can target
|
|
828
|
+
* the same provider with different scopes
|
|
829
|
+
* (e.g. `google-calendar` vs `google-drive`).
|
|
830
|
+
*
|
|
831
|
+
* Configuration: per-provider client id + secret come from env
|
|
832
|
+
* (`PYLON_OAUTH_<PROVIDER>_CLIENT_ID`, `PYLON_OAUTH_<PROVIDER>_CLIENT_SECRET`).
|
|
833
|
+
* Callback URL is derived from `PYLON_PUBLIC_URL` +
|
|
834
|
+
* `/api/connections/<name>/callback`.
|
|
835
|
+
*
|
|
836
|
+
* Storage: the framework auto-creates a `_Connection` entity at
|
|
837
|
+
* boot when any connection is declared; token fields are AEAD-
|
|
838
|
+
* encrypted at rest (`PYLON_ENCRYPTION_KEY` is REQUIRED — boot
|
|
839
|
+
* fails without it when connections are declared).
|
|
840
|
+
*/
|
|
841
|
+
export type ConnectionConfig = {
|
|
842
|
+
/** App-facing key. `ctx.connections.get(name)` matches on this. */
|
|
843
|
+
name: string;
|
|
844
|
+
/** Provider identifier matching pylon-auth's OAuth client. */
|
|
845
|
+
provider: string;
|
|
846
|
+
/** Whitespace-separated scopes. Empty = provider default. */
|
|
847
|
+
scopes?: string;
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
export type ManifestConnection = {
|
|
851
|
+
name: string;
|
|
852
|
+
provider: string;
|
|
853
|
+
scopes?: string;
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Declare a server-side OAuth integration. Returns the manifest
|
|
858
|
+
* entry the runtime parses. Re-exported through `app.ts`:
|
|
859
|
+
*
|
|
860
|
+
* ```ts
|
|
861
|
+
* import { defineConnection } from "@pylonsync/sdk";
|
|
862
|
+
*
|
|
863
|
+
* export const googleConn = defineConnection({
|
|
864
|
+
* name: "google",
|
|
865
|
+
* provider: "google",
|
|
866
|
+
* scopes: "email profile https://www.googleapis.com/auth/calendar.readonly",
|
|
867
|
+
* });
|
|
868
|
+
* ```
|
|
869
|
+
*
|
|
870
|
+
* `buildManifest({ connections: [googleConn] })` carries this into
|
|
871
|
+
* the manifest; the runtime auto-creates the `_Connection` entity
|
|
872
|
+
* and exposes `ctx.connections.get("google")` to actions.
|
|
873
|
+
*/
|
|
874
|
+
export function defineConnection(cfg: ConnectionConfig): ManifestConnection {
|
|
875
|
+
return {
|
|
876
|
+
name: cfg.name,
|
|
877
|
+
provider: cfg.provider,
|
|
878
|
+
...(cfg.scopes ? { scopes: cfg.scopes } : {}),
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Build the manifest's `llm` block from the user-facing camelCase
|
|
884
|
+
* config. Returns the snake_case shape the Rust runtime parses.
|
|
885
|
+
*
|
|
886
|
+
* ```ts
|
|
887
|
+
* export default {
|
|
888
|
+
* llm: llm({
|
|
889
|
+
* provider: "anthropic",
|
|
890
|
+
* defaultModel: "claude-sonnet-4-5",
|
|
891
|
+
* allowedModels: ["claude-sonnet-4-5", "claude-haiku-4-5"],
|
|
892
|
+
* }),
|
|
893
|
+
* }
|
|
894
|
+
* ```
|
|
895
|
+
*/
|
|
896
|
+
export function llm(cfg: LlmConfig = {}): ManifestLlmConfig {
|
|
897
|
+
const out: ManifestLlmConfig = {};
|
|
898
|
+
if (cfg.provider) out.provider = cfg.provider;
|
|
899
|
+
if (cfg.defaultModel) out.default_model = cfg.defaultModel;
|
|
900
|
+
if (cfg.allowedModels && cfg.allowedModels.length > 0) {
|
|
901
|
+
out.allowed_models = cfg.allowedModels;
|
|
902
|
+
}
|
|
903
|
+
return out;
|
|
904
|
+
}
|
|
905
|
+
|
|
738
906
|
export type ManifestAuthConfig = {
|
|
739
907
|
user: {
|
|
740
908
|
entity: string;
|
|
@@ -801,6 +969,8 @@ export function buildManifest(options: {
|
|
|
801
969
|
actions?: ActionDefinition[];
|
|
802
970
|
policies?: PolicyDefinition[];
|
|
803
971
|
auth?: ManifestAuthConfig;
|
|
972
|
+
llm?: ManifestLlmConfig;
|
|
973
|
+
connections?: ManifestConnection[];
|
|
804
974
|
}): AppManifest {
|
|
805
975
|
// Pull policies attached via the fluent `e.entity().policies(...)`
|
|
806
976
|
// chain onto the top-level policies list. Without this, fluent
|
|
@@ -839,6 +1009,12 @@ export function buildManifest(options: {
|
|
|
839
1009
|
actions: actionsToManifest(options.actions ?? []),
|
|
840
1010
|
policies: policiesToManifest(allPolicies),
|
|
841
1011
|
auth: options.auth ?? auth(),
|
|
1012
|
+
...(options.llm && Object.keys(options.llm).length > 0
|
|
1013
|
+
? { llm: options.llm }
|
|
1014
|
+
: {}),
|
|
1015
|
+
...(options.connections && options.connections.length > 0
|
|
1016
|
+
? { connections: options.connections }
|
|
1017
|
+
: {}),
|
|
842
1018
|
};
|
|
843
1019
|
}
|
|
844
1020
|
|
|
@@ -1049,6 +1225,9 @@ function buildFieldWithDefaults(
|
|
|
1049
1225
|
readonly() {
|
|
1050
1226
|
return buildFieldWithDefaults({ ...def, readonly: true });
|
|
1051
1227
|
},
|
|
1228
|
+
encrypted() {
|
|
1229
|
+
return buildFieldWithDefaults({ ...def, encrypted: true });
|
|
1230
|
+
},
|
|
1052
1231
|
default(value: unknown) {
|
|
1053
1232
|
return buildFieldWithDefaults({
|
|
1054
1233
|
...def,
|