@gencow/core 0.1.0 → 0.1.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.
@@ -53,6 +53,8 @@ export interface QueryDef<TSchema = any, TReturn = any> {
53
53
  key: string;
54
54
  handler: QueryHandler<InferArgs<TSchema>, TReturn>;
55
55
  argsSchema?: TSchema;
56
+ /** true = 인증 없이 접근 가능, false(기본) = auth 필수 (Secure by Default) */
57
+ isPublic: boolean;
56
58
  _args?: InferArgs<TSchema>;
57
59
  _return?: TReturn;
58
60
  }
@@ -60,16 +62,28 @@ export interface MutationDef<TSchema = any, TReturn = any> {
60
62
  invalidates: string[];
61
63
  handler: MutationHandler<InferArgs<TSchema>, TReturn>;
62
64
  argsSchema?: TSchema;
65
+ /** true = 인증 없이 접근 가능, false(기본) = auth 필수 (Secure by Default) */
66
+ isPublic: boolean;
63
67
  _args?: InferArgs<TSchema>;
64
68
  _return?: TReturn;
65
69
  }
70
+ declare global {
71
+ var __gencow_queryRegistry: Map<string, QueryDef<any, any>>;
72
+ var __gencow_mutationRegistry: (MutationDef<any, any> & {
73
+ name: string;
74
+ })[];
75
+ var __gencow_subscribers: Map<string, Set<WSContext>>;
76
+ var __gencow_connectedClients: Set<WSContext>;
77
+ }
66
78
  export declare function query<TSchema = any, TReturn = any>(key: string, handlerOrDef: QueryHandler<InferArgs<TSchema>, TReturn> | {
67
79
  args?: TSchema;
80
+ public?: boolean;
68
81
  handler: QueryHandler<InferArgs<TSchema>, TReturn>;
69
82
  }): QueryDef<TSchema, TReturn>;
70
83
  export declare function mutation<TSchema = any, TReturn = any>(invalidatesOrDef: string[] | {
71
84
  name?: string;
72
85
  args?: TSchema;
86
+ public?: boolean;
73
87
  invalidates: string[];
74
88
  handler: MutationHandler<InferArgs<TSchema>, TReturn>;
75
89
  }, handler?: MutationHandler<InferArgs<TSchema>, TReturn>, name?: string): MutationDef<TSchema, TReturn>;
package/dist/reactive.js CHANGED
@@ -1,25 +1,34 @@
1
- // ─── Registry ───────────────────────────────────────────
2
- const queryRegistry = new Map();
3
- const mutationRegistry = [];
4
- const subscribers = new Map();
1
+ if (!globalThis.__gencow_queryRegistry)
2
+ globalThis.__gencow_queryRegistry = new Map();
3
+ if (!globalThis.__gencow_mutationRegistry)
4
+ globalThis.__gencow_mutationRegistry = [];
5
+ if (!globalThis.__gencow_subscribers)
6
+ globalThis.__gencow_subscribers = new Map();
7
+ if (!globalThis.__gencow_connectedClients)
8
+ globalThis.__gencow_connectedClients = new Set();
9
+ const queryRegistry = globalThis.__gencow_queryRegistry;
10
+ const mutationRegistry = globalThis.__gencow_mutationRegistry;
11
+ const subscribers = globalThis.__gencow_subscribers;
5
12
  /**
6
13
  * Every WebSocket client that ever establishes a connection is tracked here.
7
14
  * This allows broadcasting invalidation events to clients that never subscribed
8
15
  * to a specific query (e.g. the admin dashboard's raw WebSocket connection).
9
16
  */
10
- const connectedClients = new Set();
17
+ const connectedClients = globalThis.__gencow_connectedClients;
11
18
  // ─── Public API (Convex-style) ──────────────────────────
12
19
  export function query(key, handlerOrDef) {
13
20
  let handler;
14
21
  let argsSchema;
22
+ let isPublic = false;
15
23
  if (typeof handlerOrDef === "function") {
16
24
  handler = handlerOrDef;
17
25
  }
18
26
  else {
19
27
  handler = handlerOrDef.handler;
20
28
  argsSchema = handlerOrDef.args;
29
+ isPublic = handlerOrDef.public === true;
21
30
  }
22
- const def = { key, handler, argsSchema };
31
+ const def = { key, handler, argsSchema, isPublic };
23
32
  queryRegistry.set(key, def);
24
33
  return def;
25
34
  }
@@ -29,6 +38,7 @@ export function mutation(invalidatesOrDef, handler, name) {
29
38
  let argsSchema;
30
39
  let actualHandler;
31
40
  let mutName;
41
+ let isPublic = false;
32
42
  if (Array.isArray(invalidatesOrDef)) {
33
43
  // Legacy style: mutation([...], handler, "name")
34
44
  invalidates = invalidatesOrDef;
@@ -36,17 +46,19 @@ export function mutation(invalidatesOrDef, handler, name) {
36
46
  mutName = name || `mutation_${++mutationCounter}`;
37
47
  }
38
48
  else {
39
- // New object style: mutation({ name?, invalidates, args?, handler })
49
+ // New object style: mutation({ name?, invalidates, args?, public?, handler })
40
50
  invalidates = invalidatesOrDef.invalidates;
41
51
  actualHandler = invalidatesOrDef.handler;
42
52
  argsSchema = invalidatesOrDef.args;
53
+ isPublic = invalidatesOrDef.public === true;
43
54
  mutName = invalidatesOrDef.name || name || `mutation_${++mutationCounter}`;
44
55
  }
45
56
  const def = {
46
57
  name: mutName,
47
58
  invalidates,
48
59
  handler: actualHandler,
49
- argsSchema
60
+ argsSchema,
61
+ isPublic,
50
62
  };
51
63
  mutationRegistry.push(def);
52
64
  return def;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gencow/core",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Gencow core library — defineQuery, defineMutation, reactive subscriptions",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -170,3 +170,68 @@ describe("emit() 방식과 legacy invalidateQueries() 혼용", () => {
170
170
  deregisterClient(wsConnected);
171
171
  });
172
172
  });
173
+
174
+ // ─── Secure by Default: public 플래그 테스트 ─────────────────────────────────
175
+
176
+ import { query, mutation, getQueryDef, getRegisteredMutations } from "../reactive";
177
+
178
+ describe("Secure by Default — public 플래그", () => {
179
+ it("query() 기본값은 isPublic === false (auth 필수)", () => {
180
+ const q = query("sectest.private", {
181
+ handler: async () => [],
182
+ });
183
+ expect(q.isPublic).toBe(false);
184
+ });
185
+
186
+ it("query({ public: true }) 시 isPublic === true", () => {
187
+ const q = query("sectest.public", {
188
+ public: true,
189
+ handler: async () => [],
190
+ });
191
+ expect(q.isPublic).toBe(true);
192
+ });
193
+
194
+ it("query() legacy handler 형식도 isPublic === false", () => {
195
+ const q = query("sectest.legacy", async () => []);
196
+ expect(q.isPublic).toBe(false);
197
+ });
198
+
199
+ it("getQueryDef()로 조회해도 isPublic 정보가 유지된다", () => {
200
+ query("sectest.lookup", { public: true, handler: async () => "ok" });
201
+ const def = getQueryDef("sectest.lookup");
202
+ expect(def).toBeDefined();
203
+ expect(def!.isPublic).toBe(true);
204
+ });
205
+
206
+ it("mutation() 기본값은 isPublic === false", () => {
207
+ const m = mutation({
208
+ name: "sectest.mut.private",
209
+ invalidates: [],
210
+ handler: async () => ({ ok: true }),
211
+ });
212
+ expect(m.isPublic).toBe(false);
213
+ });
214
+
215
+ it("mutation({ public: true }) 시 isPublic === true", () => {
216
+ const m = mutation({
217
+ name: "sectest.mut.public",
218
+ invalidates: [],
219
+ public: true,
220
+ handler: async () => ({ ok: true }),
221
+ });
222
+ expect(m.isPublic).toBe(true);
223
+ });
224
+
225
+ it("mutation() legacy array 형식도 isPublic === false", () => {
226
+ const m = mutation(["some.key"], async () => ({ ok: true }), "sectest.mut.legacy");
227
+ expect(m.isPublic).toBe(false);
228
+ });
229
+
230
+ it("getRegisteredMutations()에서 isPublic 정보가 노출된다", () => {
231
+ const all = getRegisteredMutations();
232
+ const pub = all.find(m => m.name === "sectest.mut.public");
233
+ const priv = all.find(m => m.name === "sectest.mut.private");
234
+ expect(pub?.isPublic).toBe(true);
235
+ expect(priv?.isPublic).toBe(false);
236
+ });
237
+ });
package/src/reactive.ts CHANGED
@@ -65,6 +65,8 @@ export interface QueryDef<TSchema = any, TReturn = any> {
65
65
  key: string;
66
66
  handler: QueryHandler<InferArgs<TSchema>, TReturn>;
67
67
  argsSchema?: TSchema;
68
+ /** true = 인증 없이 접근 가능, false(기본) = auth 필수 (Secure by Default) */
69
+ isPublic: boolean;
68
70
  _args?: InferArgs<TSchema>;
69
71
  _return?: TReturn;
70
72
  }
@@ -73,40 +75,64 @@ export interface MutationDef<TSchema = any, TReturn = any> {
73
75
  invalidates: string[];
74
76
  handler: MutationHandler<InferArgs<TSchema>, TReturn>;
75
77
  argsSchema?: TSchema;
78
+ /** true = 인증 없이 접근 가능, false(기본) = auth 필수 (Secure by Default) */
79
+ isPublic: boolean;
76
80
  _args?: InferArgs<TSchema>;
77
81
  _return?: TReturn;
78
82
  }
79
83
 
80
84
  // ─── Registry ───────────────────────────────────────────
85
+ //
86
+ // globalThis 기반 — 서버 번들(인라인)과 node_modules/@gencow/core 양쪽에서
87
+ // 동일한 레지스트리 인스턴스를 공유. Dual-Module Registry 버그 방지.
88
+ // See: docs/analysis/analysis-dual-module-registry.md
89
+
90
+ declare global {
91
+ // eslint-disable-next-line no-var
92
+ var __gencow_queryRegistry: Map<string, QueryDef<any, any>>;
93
+ // eslint-disable-next-line no-var
94
+ var __gencow_mutationRegistry: (MutationDef<any, any> & { name: string })[];
95
+ // eslint-disable-next-line no-var
96
+ var __gencow_subscribers: Map<string, Set<WSContext>>;
97
+ // eslint-disable-next-line no-var
98
+ var __gencow_connectedClients: Set<WSContext>;
99
+ }
100
+
101
+ if (!globalThis.__gencow_queryRegistry) globalThis.__gencow_queryRegistry = new Map();
102
+ if (!globalThis.__gencow_mutationRegistry) globalThis.__gencow_mutationRegistry = [];
103
+ if (!globalThis.__gencow_subscribers) globalThis.__gencow_subscribers = new Map();
104
+ if (!globalThis.__gencow_connectedClients) globalThis.__gencow_connectedClients = new Set();
81
105
 
82
- const queryRegistry = new Map<string, QueryDef<any, any>>();
83
- const mutationRegistry: (MutationDef<any, any> & { name: string })[] = [];
84
- const subscribers = new Map<string, Set<WSContext>>();
106
+ const queryRegistry = globalThis.__gencow_queryRegistry;
107
+ const mutationRegistry = globalThis.__gencow_mutationRegistry;
108
+ const subscribers = globalThis.__gencow_subscribers;
85
109
 
86
110
  /**
87
111
  * Every WebSocket client that ever establishes a connection is tracked here.
88
112
  * This allows broadcasting invalidation events to clients that never subscribed
89
113
  * to a specific query (e.g. the admin dashboard's raw WebSocket connection).
90
114
  */
91
- const connectedClients = new Set<WSContext>();
115
+ const connectedClients = globalThis.__gencow_connectedClients;
92
116
 
93
117
  // ─── Public API (Convex-style) ──────────────────────────
94
118
 
95
119
  export function query<TSchema = any, TReturn = any>(
96
120
  key: string,
97
- handlerOrDef: QueryHandler<InferArgs<TSchema>, TReturn> | { args?: TSchema; handler: QueryHandler<InferArgs<TSchema>, TReturn> }
121
+ handlerOrDef: QueryHandler<InferArgs<TSchema>, TReturn> | { args?: TSchema; public?: boolean; handler: QueryHandler<InferArgs<TSchema>, TReturn> }
98
122
  ): QueryDef<TSchema, TReturn> {
99
123
  let handler: QueryHandler<InferArgs<TSchema>, TReturn>;
100
124
  let argsSchema: TSchema | undefined;
125
+ let isPublic = false;
101
126
 
102
127
  if (typeof handlerOrDef === "function") {
103
128
  handler = handlerOrDef;
104
129
  } else {
105
130
  handler = handlerOrDef.handler;
106
131
  argsSchema = handlerOrDef.args;
132
+ isPublic = handlerOrDef.public === true;
107
133
  }
108
134
 
109
- const def: QueryDef<TSchema, TReturn> = { key, handler, argsSchema };
135
+ const def: QueryDef<TSchema, TReturn> = { key, handler, argsSchema, isPublic };
110
136
  queryRegistry.set(key, def);
111
137
  return def;
112
138
  }
@@ -114,7 +140,7 @@ export function query<TSchema = any, TReturn = any>(
114
140
  let mutationCounter = 0;
115
141
 
116
142
  export function mutation<TSchema = any, TReturn = any>(
117
- invalidatesOrDef: string[] | { name?: string; args?: TSchema; invalidates: string[]; handler: MutationHandler<InferArgs<TSchema>, TReturn> },
143
+ invalidatesOrDef: string[] | { name?: string; args?: TSchema; public?: boolean; invalidates: string[]; handler: MutationHandler<InferArgs<TSchema>, TReturn> },
118
144
  handler?: MutationHandler<InferArgs<TSchema>, TReturn>,
119
145
  name?: string
120
146
  ): MutationDef<TSchema, TReturn> {
@@ -122,6 +148,7 @@ export function mutation<TSchema = any, TReturn = any>(
122
148
  let argsSchema: TSchema | undefined;
123
149
  let actualHandler: MutationHandler<InferArgs<TSchema>, TReturn>;
124
150
  let mutName: string;
151
+ let isPublic = false;
125
152
 
126
153
  if (Array.isArray(invalidatesOrDef)) {
127
154
  // Legacy style: mutation([...], handler, "name")
@@ -129,17 +156,19 @@ export function mutation<TSchema = any, TReturn = any>(
129
156
  actualHandler = handler!;
130
157
  mutName = name || `mutation_${++mutationCounter}`;
131
158
  } else {
132
- // New object style: mutation({ name?, invalidates, args?, handler })
159
+ // New object style: mutation({ name?, invalidates, args?, public?, handler })
133
160
  invalidates = invalidatesOrDef.invalidates;
134
161
  actualHandler = invalidatesOrDef.handler;
135
162
  argsSchema = invalidatesOrDef.args;
163
+ isPublic = invalidatesOrDef.public === true;
136
164
  mutName = invalidatesOrDef.name || name || `mutation_${++mutationCounter}`;
137
165
  }
138
166
  const def: MutationDef<TSchema, TReturn> & { name: string } = {
139
167
  name: mutName,
140
168
  invalidates,
141
169
  handler: actualHandler,
142
- argsSchema
170
+ argsSchema,
171
+ isPublic,
143
172
  };
144
173
  mutationRegistry.push(def);
145
174
  return def;