@pylonsync/sdk 0.3.197 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +179 -0
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.197",
6
+ "version": "0.3.198",
7
7
  "type": "module",
8
8
  "main": "src/index.ts",
9
9
  "types": "src/index.ts",
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,