@tinacms/graphql 2.4.11 → 4.0.0

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.
@@ -130,6 +130,7 @@ export declare const astBuilder: {
130
130
  SystemInfo: string;
131
131
  Boolean: string;
132
132
  JSON: string;
133
+ RichText: string;
133
134
  Node: string;
134
135
  PageInfo: string;
135
136
  Connection: string;
@@ -20,6 +20,16 @@ export interface DatabaseArgs {
20
20
  version?: boolean;
21
21
  namespace?: string;
22
22
  levelBatchSize?: number;
23
+ authCollection?: AuthCollectionOptions;
24
+ }
25
+ export interface AuthCollectionOptions {
26
+ /** Values of the uid field of users who can edit the auth collection. */
27
+ admins?: string[];
28
+ /**
29
+ * Allow writes to the auth collection from calls that pass no ctxUser.
30
+ * A null ctxUser means nobody is signed in, and is still refused.
31
+ */
32
+ allowUnauthenticatedWrites?: boolean;
23
33
  }
24
34
  export interface GitProvider {
25
35
  onPut: (key: string, value: string) => Promise<void>;
@@ -47,6 +57,10 @@ export type CreateLocalDatabaseArgs = Omit<DatabaseArgs, 'level'> & {
47
57
  };
48
58
  export declare const createLocalDatabase: (config?: CreateLocalDatabaseArgs) => Database;
49
59
  export declare const createDatabase: (config: CreateDatabase) => Database;
60
+ /**
61
+ * Builds a database for CLI tooling and tests. It trusts requests without a
62
+ * user, so do not use it for a public endpoint. Use `createDatabase` instead.
63
+ */
50
64
  export declare const createDatabaseInternal: (config: DatabaseArgs) => Database;
51
65
  /** Options for {@link Database.query} **/
52
66
  export type QueryOptions = {
@@ -71,6 +85,10 @@ export declare class Database {
71
85
  private readonly onPut;
72
86
  private readonly onDelete;
73
87
  private readonly levelBatchSize;
88
+ readonly authCollection: Readonly<{
89
+ admins: readonly string[];
90
+ allowUnauthenticatedWrites: boolean;
91
+ }>;
74
92
  private tinaSchema;
75
93
  private contentNamespace;
76
94
  private collectionIndexDefinitions;
package/dist/index.js CHANGED
@@ -505,6 +505,7 @@ var astBuilder = {
505
505
  SystemInfo: "SystemInfo",
506
506
  Boolean: "Boolean",
507
507
  JSON: "JSON",
508
+ RichText: "RichText",
508
509
  Node: "Node",
509
510
  PageInfo: "PageInfo",
510
511
  Connection: "Connection",
@@ -1173,6 +1174,10 @@ var scalarDefinitions = [
1173
1174
  description: "References another document, used as a foreign key"
1174
1175
  }),
1175
1176
  astBuilder.ScalarTypeDefinition({ name: "JSON" }),
1177
+ astBuilder.ScalarTypeDefinition({
1178
+ name: "RichText",
1179
+ description: "A rich-text document, renderable by TinaMarkdown"
1180
+ }),
1176
1181
  astBuilder.ObjectTypeDefinition({
1177
1182
  name: "SystemInfo",
1178
1183
  fields: [
@@ -2550,7 +2555,7 @@ var Builder = class {
2550
2555
  return astBuilder.InputValueDefinition({
2551
2556
  name: field.name,
2552
2557
  list: field.list,
2553
- type: astBuilder.TYPES.JSON
2558
+ type: astBuilder.TYPES.RichText
2554
2559
  });
2555
2560
  case "reference":
2556
2561
  return astBuilder.InputValueDefinition({
@@ -2799,7 +2804,7 @@ Visit https://tina.io/docs/r/content-fields/#list-fields/ for more information
2799
2804
  name: field.name,
2800
2805
  list: field.list,
2801
2806
  required: field.required,
2802
- type: astBuilder.TYPES.JSON
2807
+ type: astBuilder.TYPES.RichText
2803
2808
  });
2804
2809
  case "reference":
2805
2810
  const name = NAMER.documentTypeName(field.namespace);
@@ -3053,7 +3058,7 @@ var assertCheckboxGroupIsList = (field) => {
3053
3058
  var package_default = {
3054
3059
  name: "@tinacms/graphql",
3055
3060
  type: "module",
3056
- version: "2.4.11",
3061
+ version: "4.0.0",
3057
3062
  main: "dist/index.js",
3058
3063
  module: "./dist/index.js",
3059
3064
  files: [
@@ -3343,7 +3348,7 @@ import {
3343
3348
  } from "graphql";
3344
3349
 
3345
3350
  // src/resolver/index.ts
3346
- import path3 from "path";
3351
+ import path4 from "path";
3347
3352
  import { isValid } from "date-fns";
3348
3353
  import { JSONPath as JSONPath2 } from "jsonpath-plus";
3349
3354
 
@@ -3351,7 +3356,11 @@ import { JSONPath as JSONPath2 } from "jsonpath-plus";
3351
3356
  import { parseMDX, serializeMDX } from "@tinacms/mdx";
3352
3357
 
3353
3358
  // src/resolver/index.ts
3354
- import { ERR_ALREADY_EXISTS, RELATIVE_PATH_REGEX } from "@tinacms/schema-tools";
3359
+ import {
3360
+ ERR_ALREADY_EXISTS,
3361
+ RELATIVE_PATH_REGEX,
3362
+ normalizePath as normalizePath2
3363
+ } from "@tinacms/schema-tools";
3355
3364
  import { GraphQLError as GraphQLError2 } from "graphql";
3356
3365
 
3357
3366
  // src/database/datalayer.ts
@@ -4674,6 +4683,170 @@ var collectConditionsForField = (fieldName, field, filterNode, pathExpression, c
4674
4683
  }
4675
4684
  };
4676
4685
 
4686
+ // src/resolver/auth-fields.ts
4687
+ import { randomBytes } from "crypto";
4688
+ import path3 from "path";
4689
+ async function getUserDocumentContext(tinaSchema, resolver) {
4690
+ const collection = tinaSchema.getCollections().find((c) => c.isAuthCollection);
4691
+ if (!collection) {
4692
+ throw new Error("Auth collection not found");
4693
+ }
4694
+ const userFields = mapUserFields(collection, ["_rawData"]);
4695
+ if (!userFields.length) {
4696
+ throw new Error(`No user field found in collection ${collection.name}`);
4697
+ }
4698
+ if (userFields.length > 1) {
4699
+ throw new Error(
4700
+ `Multiple user fields found in collection ${collection.name}`
4701
+ );
4702
+ }
4703
+ const userField = userFields[0];
4704
+ const relativePath = "index.json";
4705
+ const realPath = path3.join(collection.path, relativePath);
4706
+ const userDoc = await resolver.getDocument(realPath);
4707
+ const users = get(userDoc, userField.path);
4708
+ if (!users) {
4709
+ throw new Error("No users found");
4710
+ }
4711
+ return { collection, userField, users, userDoc, relativePath };
4712
+ }
4713
+ function findUserInCollection(users, userField, userSub) {
4714
+ const { idFieldName } = userField;
4715
+ if (!idFieldName) {
4716
+ throw new Error("No uid field found on user field");
4717
+ }
4718
+ return users.find((u) => u[idFieldName] === userSub) || null;
4719
+ }
4720
+ function withoutPasswordValue(user, userField) {
4721
+ const { passwordFieldName } = userField;
4722
+ if (!passwordFieldName) {
4723
+ return user;
4724
+ }
4725
+ return {
4726
+ ...user,
4727
+ [passwordFieldName]: {
4728
+ passwordChangeRequired: user[passwordFieldName]?.passwordChangeRequired ?? false
4729
+ }
4730
+ };
4731
+ }
4732
+ var dummyPasswordHash;
4733
+ var warnedUnflaggedAuthenticate = false;
4734
+ var getDummyPasswordHash = () => {
4735
+ dummyPasswordHash ??= generatePasswordHash({
4736
+ password: randomBytes(32).toString("hex")
4737
+ });
4738
+ return dummyPasswordHash;
4739
+ };
4740
+ function assertNewUsersHavePasswords(collection, newBody, existingData) {
4741
+ const userFields = mapUserFields(collection);
4742
+ if (userFields.length !== 1) {
4743
+ return;
4744
+ }
4745
+ const [{ path: usersPath, idFieldName, passwordFieldName }] = userFields;
4746
+ const users = get(newBody, usersPath);
4747
+ if (!Array.isArray(users) || !idFieldName || !passwordFieldName) {
4748
+ return;
4749
+ }
4750
+ const storedUsers = get(existingData, usersPath);
4751
+ const storedIds = new Set(
4752
+ (Array.isArray(storedUsers) ? storedUsers : []).map((u) => u?.[idFieldName])
4753
+ );
4754
+ const withoutPassword = users.filter(
4755
+ (u) => !storedIds.has(u?.[idFieldName]) && !u?.[passwordFieldName]?.["value"]
4756
+ ).map((u) => u?.[idFieldName]);
4757
+ if (withoutPassword.length) {
4758
+ throw new Error(`New users need a password: ${withoutPassword.join(", ")}`);
4759
+ }
4760
+ }
4761
+ async function handleAuthenticate({
4762
+ tinaSchema,
4763
+ resolver,
4764
+ sub,
4765
+ password,
4766
+ isSignIn
4767
+ }) {
4768
+ if (!isSignIn) {
4769
+ if (!warnedUnflaggedAuthenticate) {
4770
+ warnedUnflaggedAuthenticate = true;
4771
+ console.warn(
4772
+ "TinaCMS: authenticate was called without isSignIn, so it returned null. If sign-in is failing, run `tinacms build` with a @tinacms/cli that matches @tinacms/datalayer to regenerate the database client."
4773
+ );
4774
+ }
4775
+ return null;
4776
+ }
4777
+ const { userField, users } = await getUserDocumentContext(
4778
+ tinaSchema,
4779
+ resolver
4780
+ );
4781
+ const user = sub ? findUserInCollection(users, userField, sub) : null;
4782
+ const { passwordFieldName } = userField;
4783
+ const saltedHash = user ? get(user, [passwordFieldName || "", "value"]) : void 0;
4784
+ if (!saltedHash) {
4785
+ await checkPasswordHash({
4786
+ saltedHash: await getDummyPasswordHash(),
4787
+ password: password ?? ""
4788
+ });
4789
+ return null;
4790
+ }
4791
+ const matches = await checkPasswordHash({
4792
+ saltedHash,
4793
+ password
4794
+ });
4795
+ return matches ? withoutPasswordValue(user, userField) : null;
4796
+ }
4797
+ async function handleAuthorize({
4798
+ tinaSchema,
4799
+ resolver,
4800
+ sub,
4801
+ ctxUser
4802
+ }) {
4803
+ const userSub = sub || ctxUser?.sub;
4804
+ const { userField, users } = await getUserDocumentContext(
4805
+ tinaSchema,
4806
+ resolver
4807
+ );
4808
+ const user = findUserInCollection(users, userField, userSub);
4809
+ return user ? withoutPasswordValue(user, userField) : null;
4810
+ }
4811
+ async function handleUpdatePassword({
4812
+ tinaSchema,
4813
+ resolver,
4814
+ password,
4815
+ ctxUser
4816
+ }) {
4817
+ if (!ctxUser?.sub) {
4818
+ throw new Error("Not authorized");
4819
+ }
4820
+ if (!password) {
4821
+ throw new Error("No password provided");
4822
+ }
4823
+ const { userField, users } = await getUserDocumentContext(
4824
+ tinaSchema,
4825
+ resolver
4826
+ );
4827
+ const { idFieldName, passwordFieldName } = userField;
4828
+ if (!passwordFieldName) {
4829
+ throw new Error("No password field found on user field");
4830
+ }
4831
+ if (!users.find((u) => u[idFieldName] === ctxUser.sub)) {
4832
+ throw new Error("Not authorized");
4833
+ }
4834
+ const passwordHash = await generatePasswordHash({ password });
4835
+ await resolver.updateAuthDocumentInternal((rawData) => {
4836
+ const storedUsers = get(rawData, userField.path.slice(1));
4837
+ const user = Array.isArray(storedUsers) ? storedUsers.find((u) => u[idFieldName] === ctxUser.sub) : void 0;
4838
+ if (!user) {
4839
+ throw new Error("Not authorized");
4840
+ }
4841
+ user[passwordFieldName] = {
4842
+ value: passwordHash,
4843
+ passwordChangeRequired: false
4844
+ };
4845
+ return rawData;
4846
+ });
4847
+ return true;
4848
+ }
4849
+
4677
4850
  // src/resolver/media-utils.ts
4678
4851
  var resolveMediaCloudToRelative = (value, config = { useRelativeMedia: true }, schema) => {
4679
4852
  if (config && value) {
@@ -4755,6 +4928,7 @@ var hasTinaMediaConfig = (schema) => {
4755
4928
  };
4756
4929
 
4757
4930
  // src/resolver/index.ts
4931
+ var authCollectionWrites = /* @__PURE__ */ new WeakMap();
4758
4932
  var createResolver = (args) => {
4759
4933
  return new Resolver(args);
4760
4934
  };
@@ -4884,6 +5058,7 @@ var resolveFieldData = async ({ namespace, ...field }, rawData, accumulator, tin
4884
5058
  };
4885
5059
  var transformDocumentIntoPayload = async (fullPath, rawData, tinaSchema, config, isAudit, hasReferences) => {
4886
5060
  const collection = tinaSchema.getCollection(rawData._collection);
5061
+ const normalizedPath = normalizePath2(fullPath);
4887
5062
  try {
4888
5063
  const template = tinaSchema.getTemplateForData({
4889
5064
  data: rawData,
@@ -4893,8 +5068,8 @@ var transformDocumentIntoPayload = async (fullPath, rawData, tinaSchema, config,
4893
5068
  base: basename,
4894
5069
  ext: extension,
4895
5070
  name: filename
4896
- } = path3.parse(fullPath);
4897
- const relativePath = fullPath.replace(/\\/g, "/").replace(collection.path, "").replace(/^\/|\/$/g, "");
5071
+ } = path4.parse(normalizedPath);
5072
+ const relativePath = normalizedPath.replace(collection.path, "").replace(/^\/|\/$/g, "");
4898
5073
  const breadcrumbs = relativePath.replace(extension, "").split("/");
4899
5074
  const data = {
4900
5075
  _collection: rawData._collection,
@@ -4928,7 +5103,7 @@ var transformDocumentIntoPayload = async (fullPath, rawData, tinaSchema, config,
4928
5103
  const title = data[titleFieldName || " "] || null;
4929
5104
  return {
4930
5105
  __typename: collection.fields ? NAMER.documentTypeName(collection.namespace) : NAMER.documentTypeName(template.namespace),
4931
- id: fullPath,
5106
+ id: normalizedPath,
4932
5107
  ...data,
4933
5108
  _sys: {
4934
5109
  title: title || "",
@@ -4936,7 +5111,7 @@ var transformDocumentIntoPayload = async (fullPath, rawData, tinaSchema, config,
4936
5111
  filename,
4937
5112
  extension,
4938
5113
  hasReferences,
4939
- path: fullPath,
5114
+ path: normalizedPath,
4940
5115
  relativePath,
4941
5116
  breadcrumbs,
4942
5117
  collection,
@@ -4990,12 +5165,14 @@ var Resolver = class _Resolver {
4990
5165
  this.database = init.database;
4991
5166
  this.tinaSchema = init.tinaSchema;
4992
5167
  this.isAudit = init.isAudit;
5168
+ this.ctxUser = init.ctxUser;
4993
5169
  }
4994
5170
  init;
4995
5171
  config;
4996
5172
  database;
4997
5173
  tinaSchema;
4998
5174
  isAudit;
5175
+ ctxUser;
4999
5176
  resolveCollection = async (_args, collectionName, hasDocuments) => {
5000
5177
  const collection = this.tinaSchema.getCollection(collectionName);
5001
5178
  const extraFields = {};
@@ -5172,6 +5349,11 @@ var Resolver = class _Resolver {
5172
5349
  collectionName,
5173
5350
  relativePath
5174
5351
  );
5352
+ await this.assertAuthCollectionWrite({
5353
+ collection,
5354
+ realPath,
5355
+ op: "addPending"
5356
+ });
5175
5357
  const alreadyExists = await this.database.documentExists(realPath);
5176
5358
  if (alreadyExists) {
5177
5359
  throw new Error(
@@ -5287,19 +5469,19 @@ var Resolver = class _Resolver {
5287
5469
  if (fullPath.includes("\0")) {
5288
5470
  throw new Error("Invalid path: null bytes are not allowed");
5289
5471
  }
5290
- const normalizedPath = path3.normalize(fullPath.replace(/\\/g, "/"));
5291
- const normalizedCollectionPath = path3.normalize(collection.path);
5292
- const relative = path3.relative(normalizedCollectionPath, normalizedPath);
5472
+ const normalizedPath = path4.normalize(fullPath.replace(/\\/g, "/"));
5473
+ const normalizedCollectionPath = path4.normalize(collection.path);
5474
+ const relative = path4.relative(normalizedCollectionPath, normalizedPath);
5293
5475
  if (relative.startsWith("..")) {
5294
5476
  throw new Error(`Invalid path: path escapes the collection directory`);
5295
5477
  }
5296
- if (path3.isAbsolute(relative)) {
5478
+ if (path4.isAbsolute(relative)) {
5297
5479
  throw new Error(`Invalid path: absolute paths are not allowed`);
5298
5480
  }
5299
5481
  if (relativePath) {
5300
5482
  _Resolver.validateRelativePath(relativePath);
5301
5483
  const collectionFormat = collection.format || "md";
5302
- const fileExtension = path3.extname(relativePath).toLowerCase().slice(1);
5484
+ const fileExtension = path4.extname(relativePath).toLowerCase().slice(1);
5303
5485
  if (fileExtension !== collectionFormat) {
5304
5486
  throw new Error(
5305
5487
  `Invalid file extension: expected '.${collectionFormat}' but got '.${fileExtension}'`
@@ -5307,6 +5489,81 @@ var Resolver = class _Resolver {
5307
5489
  }
5308
5490
  }
5309
5491
  };
5492
+ assertAuthCollectionWrite = async ({
5493
+ collection,
5494
+ realPath,
5495
+ op
5496
+ }) => {
5497
+ const authCollection = this.tinaSchema.getCollections().find((c) => c.isAuthCollection);
5498
+ if (!authCollection) {
5499
+ return;
5500
+ }
5501
+ const normalizedPath = normalizePath2(realPath);
5502
+ if (collection.name !== authCollection.name && !this.isStoredInCollection(normalizedPath, authCollection)) {
5503
+ return;
5504
+ }
5505
+ const { admins, allowUnauthenticatedWrites } = this.database.authCollection ?? { admins: [], allowUnauthenticatedWrites: true };
5506
+ if (this.ctxUser === void 0 && allowUnauthenticatedWrites) {
5507
+ return;
5508
+ }
5509
+ const sub = this.ctxUser?.sub;
5510
+ const userStorePath = normalizePath2(
5511
+ path4.join(authCollection.path, "index.json")
5512
+ );
5513
+ if (op === "update" && normalizedPath === userStorePath && typeof sub === "string" && sub !== "" && admins.includes(sub) && await this.isStoredUser(sub)) {
5514
+ return;
5515
+ }
5516
+ throw new Error("Not authorized");
5517
+ };
5518
+ assertReferenceUpdates = async (realPath, collection) => {
5519
+ if (!this.tinaSchema.getCollections().some((c) => c.isAuthCollection)) {
5520
+ return;
5521
+ }
5522
+ const collRefs = await this.findReferences(realPath, collection);
5523
+ for (const docsWithRefs of Object.values(collRefs)) {
5524
+ for (const pathToDocWithRef of Object.keys(docsWithRefs)) {
5525
+ await this.assertAuthCollectionWrite({
5526
+ collection: this.tinaSchema.getCollectionByFullPath(pathToDocWithRef),
5527
+ realPath: pathToDocWithRef,
5528
+ op: "update"
5529
+ });
5530
+ }
5531
+ }
5532
+ };
5533
+ isAuthCollectionPath = (realPath) => {
5534
+ const authCollection = this.tinaSchema.getCollections().find((c) => c.isAuthCollection);
5535
+ return !!authCollection && this.isStoredInCollection(normalizePath2(realPath), authCollection);
5536
+ };
5537
+ withAuthCollectionLock = (realPath, write) => {
5538
+ if (!this.isAuthCollectionPath(realPath)) {
5539
+ return write();
5540
+ }
5541
+ const previous = authCollectionWrites.get(this.database);
5542
+ const current = previous ? previous.then(write, write) : write();
5543
+ authCollectionWrites.set(
5544
+ this.database,
5545
+ current.catch(() => void 0)
5546
+ );
5547
+ return current;
5548
+ };
5549
+ isStoredInCollection = (realPath, collection) => {
5550
+ try {
5551
+ return this.tinaSchema.getCollectionByFullPath(realPath)?.name === collection.name;
5552
+ } catch {
5553
+ return false;
5554
+ }
5555
+ };
5556
+ isStoredUser = async (sub) => {
5557
+ try {
5558
+ const { userField, users } = await getUserDocumentContext(
5559
+ this.tinaSchema,
5560
+ this
5561
+ );
5562
+ return Array.isArray(users) && !!findUserInCollection(users, userField, sub);
5563
+ } catch {
5564
+ return false;
5565
+ }
5566
+ };
5310
5567
  /**
5311
5568
  * Helper method to get collection and construct validated path.
5312
5569
  * This encapsulates the common pattern of getting a collection, joining paths,
@@ -5315,7 +5572,9 @@ var Resolver = class _Resolver {
5315
5572
  * @param collectionName - Name of the collection
5316
5573
  * @param relativePath - Relative path within the collection
5317
5574
  * @param options - Optional configuration
5318
- * @returns Object containing the collection and validated real path
5575
+ * @returns Object containing the collection and the validated real path,
5576
+ * normalized to POSIX separators so it matches the document identities and
5577
+ * reference values stored in content files on every platform
5319
5578
  */
5320
5579
  getValidatedPath = (collectionName, relativePath, options) => {
5321
5580
  _Resolver.validateRelativePath(relativePath);
@@ -5325,7 +5584,7 @@ var Resolver = class _Resolver {
5325
5584
  if (options?.extraSegments) {
5326
5585
  pathSegments.push(...options.extraSegments.map(_Resolver.sanitizePath));
5327
5586
  }
5328
- const realPath = path3.join(...pathSegments);
5587
+ const realPath = normalizePath2(path4.join(...pathSegments));
5329
5588
  const shouldValidateExtension = options?.validateExtension !== false;
5330
5589
  this.validatePath(
5331
5590
  realPath,
@@ -5359,12 +5618,17 @@ var Resolver = class _Resolver {
5359
5618
  }) => {
5360
5619
  const collection = this.getCollectionWithName(collectionName);
5361
5620
  _Resolver.validateRelativePath(relativePath);
5362
- const realPath = path3.join(
5621
+ const realPath = path4.join(
5363
5622
  collection.path,
5364
5623
  relativePath,
5365
5624
  `.gitkeep.${collection.format || "md"}`
5366
5625
  );
5367
5626
  this.validatePath(realPath, collection);
5627
+ await this.assertAuthCollectionWrite({
5628
+ collection,
5629
+ realPath,
5630
+ op: "createFolder"
5631
+ });
5368
5632
  const alreadyExists = await this.database.documentExists(realPath);
5369
5633
  if (alreadyExists) {
5370
5634
  throw new Error(
@@ -5387,6 +5651,11 @@ var Resolver = class _Resolver {
5387
5651
  collectionName,
5388
5652
  relativePath
5389
5653
  );
5654
+ await this.assertAuthCollectionWrite({
5655
+ collection,
5656
+ realPath,
5657
+ op: "create"
5658
+ });
5390
5659
  const alreadyExists = await this.database.documentExists(realPath);
5391
5660
  if (alreadyExists) {
5392
5661
  throw new Error(
@@ -5397,7 +5666,17 @@ var Resolver = class _Resolver {
5397
5666
  await this.database.put(realPath, params, collection.name);
5398
5667
  return this.getDocument(realPath);
5399
5668
  };
5400
- resolveUpdateDocument = async ({
5669
+ resolveUpdateDocument = async (args) => {
5670
+ const { realPath } = this.getValidatedPath(
5671
+ args.collectionName,
5672
+ args.relativePath
5673
+ );
5674
+ return this.withAuthCollectionLock(
5675
+ realPath,
5676
+ () => this.updateDocument(args)
5677
+ );
5678
+ };
5679
+ updateDocument = async ({
5401
5680
  collectionName,
5402
5681
  relativePath,
5403
5682
  newRelativePath,
@@ -5407,6 +5686,13 @@ var Resolver = class _Resolver {
5407
5686
  collectionName,
5408
5687
  relativePath
5409
5688
  );
5689
+ if (!newRelativePath) {
5690
+ await this.assertAuthCollectionWrite({
5691
+ collection,
5692
+ realPath,
5693
+ op: "update"
5694
+ });
5695
+ }
5410
5696
  const alreadyExists = await this.database.documentExists(realPath);
5411
5697
  if (!alreadyExists) {
5412
5698
  throw new Error(`Unable to update document, ${realPath} does not exist`);
@@ -5426,38 +5712,53 @@ var Resolver = class _Resolver {
5426
5712
  `Unable to rename document, ${newRealPath} ${ERR_ALREADY_EXISTS}`
5427
5713
  );
5428
5714
  }
5715
+ await this.assertAuthCollectionWrite({
5716
+ collection,
5717
+ realPath,
5718
+ op: "rename"
5719
+ });
5720
+ await this.assertAuthCollectionWrite({
5721
+ collection,
5722
+ realPath: newRealPath,
5723
+ op: "rename"
5724
+ });
5725
+ await this.assertReferenceUpdates(realPath, collection);
5429
5726
  await this.database.put(newRealPath, doc._rawData, collection.name);
5430
5727
  await this.deleteDocument(realPath);
5431
5728
  const collRefs = await this.findReferences(realPath, collection);
5729
+ const holdsLock = this.isAuthCollectionPath(realPath);
5432
5730
  for (const [_collection, docsWithRefs] of Object.entries(collRefs)) {
5433
5731
  for (const [pathToDocWithRef, referencePaths] of Object.entries(
5434
5732
  docsWithRefs
5435
5733
  )) {
5436
- let docWithRef = await this.getRaw(pathToDocWithRef);
5437
- let hasUpdate = false;
5438
- for (const path9 of referencePaths) {
5439
- const { object: object2, updated } = updateObjectWithJsonPath(
5440
- docWithRef,
5441
- path9,
5442
- realPath,
5443
- newRealPath
5444
- );
5445
- docWithRef = object2;
5446
- hasUpdate = updated || hasUpdate;
5447
- }
5448
- if (hasUpdate) {
5449
- const collectionWithRef = this.tinaSchema.getCollectionByFullPath(pathToDocWithRef);
5450
- if (!collectionWithRef) {
5451
- throw new Error(
5452
- `Unable to find collection for ${pathToDocWithRef}`
5734
+ const rewriteReferences = async () => {
5735
+ let docWithRef = await this.getRaw(pathToDocWithRef);
5736
+ let hasUpdate = false;
5737
+ for (const path9 of referencePaths) {
5738
+ const { object: object2, updated } = updateObjectWithJsonPath(
5739
+ docWithRef,
5740
+ path9,
5741
+ realPath,
5742
+ newRealPath
5453
5743
  );
5744
+ docWithRef = object2;
5745
+ hasUpdate = updated || hasUpdate;
5454
5746
  }
5455
- await this.database.put(
5456
- pathToDocWithRef,
5457
- docWithRef,
5458
- collectionWithRef.name
5459
- );
5460
- }
5747
+ if (hasUpdate) {
5748
+ const collectionWithRef = this.tinaSchema.getCollectionByFullPath(pathToDocWithRef);
5749
+ if (!collectionWithRef) {
5750
+ throw new Error(
5751
+ `Unable to find collection for ${pathToDocWithRef}`
5752
+ );
5753
+ }
5754
+ await this.database.put(
5755
+ pathToDocWithRef,
5756
+ docWithRef,
5757
+ collectionWithRef.name
5758
+ );
5759
+ }
5760
+ };
5761
+ await (holdsLock ? rewriteReferences() : this.withAuthCollectionLock(pathToDocWithRef, rewriteReferences));
5461
5762
  }
5462
5763
  }
5463
5764
  return this.getDocument(newRealPath);
@@ -5465,6 +5766,9 @@ var Resolver = class _Resolver {
5465
5766
  if (!newBody) {
5466
5767
  throw new Error("Body not provided for updated document.");
5467
5768
  }
5769
+ if (collection.isAuthCollection) {
5770
+ assertNewUsersHavePasswords(collection, newBody, doc?._rawData);
5771
+ }
5468
5772
  const params = await this.buildObjectMutations(
5469
5773
  newBody,
5470
5774
  collection,
@@ -5481,6 +5785,21 @@ var Resolver = class _Resolver {
5481
5785
  );
5482
5786
  return this.getDocument(realPath);
5483
5787
  };
5788
+ /**
5789
+ * Rewrites the auth collection's user store without the admin check, so the
5790
+ * caller must check the request user.
5791
+ */
5792
+ updateAuthDocumentInternal = async (update) => {
5793
+ const collection = this.tinaSchema.getCollections().find((c) => c.isAuthCollection);
5794
+ if (!collection) {
5795
+ throw new Error("Auth collection not found");
5796
+ }
5797
+ const { realPath } = this.getValidatedPath(collection.name, "index.json");
5798
+ await this.withAuthCollectionLock(realPath, async () => {
5799
+ const rawData = await this.getRaw(realPath);
5800
+ await this.database.put(realPath, update(rawData), collection.name);
5801
+ });
5802
+ };
5484
5803
  resolveDeleteDocument = async ({
5485
5804
  collectionName,
5486
5805
  relativePath
@@ -5493,39 +5812,50 @@ var Resolver = class _Resolver {
5493
5812
  if (!alreadyExists) {
5494
5813
  throw new Error(`Unable to delete document, ${realPath} does not exist`);
5495
5814
  }
5815
+ await this.assertAuthCollectionWrite({
5816
+ collection,
5817
+ realPath,
5818
+ op: "delete"
5819
+ });
5820
+ const hasReferences = await this.hasReferences(realPath, collection);
5821
+ if (hasReferences) {
5822
+ await this.assertReferenceUpdates(realPath, collection);
5823
+ }
5496
5824
  const doc = await this.getDocument(realPath);
5497
5825
  await this.deleteDocument(realPath);
5498
- if (await this.hasReferences(realPath, collection)) {
5826
+ if (hasReferences) {
5499
5827
  const collRefs = await this.findReferences(realPath, collection);
5500
5828
  for (const [_collection, docsWithRefs] of Object.entries(collRefs)) {
5501
5829
  for (const [pathToDocWithRef, referencePaths] of Object.entries(
5502
5830
  docsWithRefs
5503
5831
  )) {
5504
- let refDoc = await this.getRaw(pathToDocWithRef);
5505
- let hasUpdate = false;
5506
- for (const path9 of referencePaths) {
5507
- const { object: object2, updated } = updateObjectWithJsonPath(
5508
- refDoc,
5509
- path9,
5510
- realPath,
5511
- null
5512
- );
5513
- refDoc = object2;
5514
- hasUpdate = updated || hasUpdate;
5515
- }
5516
- if (hasUpdate) {
5517
- const collectionWithRef = this.tinaSchema.getCollectionByFullPath(pathToDocWithRef);
5518
- if (!collectionWithRef) {
5519
- throw new Error(
5520
- `Unable to find collection for ${pathToDocWithRef}`
5832
+ await this.withAuthCollectionLock(pathToDocWithRef, async () => {
5833
+ let refDoc = await this.getRaw(pathToDocWithRef);
5834
+ let hasUpdate = false;
5835
+ for (const path9 of referencePaths) {
5836
+ const { object: object2, updated } = updateObjectWithJsonPath(
5837
+ refDoc,
5838
+ path9,
5839
+ realPath,
5840
+ null
5521
5841
  );
5842
+ refDoc = object2;
5843
+ hasUpdate = updated || hasUpdate;
5522
5844
  }
5523
- await this.database.put(
5524
- pathToDocWithRef,
5525
- refDoc,
5526
- collectionWithRef.name
5527
- );
5528
- }
5845
+ if (hasUpdate) {
5846
+ const collectionWithRef = this.tinaSchema.getCollectionByFullPath(pathToDocWithRef);
5847
+ if (!collectionWithRef) {
5848
+ throw new Error(
5849
+ `Unable to find collection for ${pathToDocWithRef}`
5850
+ );
5851
+ }
5852
+ await this.database.put(
5853
+ pathToDocWithRef,
5854
+ refDoc,
5855
+ collectionWithRef.name
5856
+ );
5857
+ }
5858
+ });
5529
5859
  }
5530
5860
  }
5531
5861
  }
@@ -5561,10 +5891,15 @@ var Resolver = class _Resolver {
5561
5891
  if (isAddPendingDocument === true) {
5562
5892
  return this.resolveAddPendingDocument({
5563
5893
  collectionName: collection.name,
5564
- relativePath: path3.relative(collection.path, realPath),
5894
+ relativePath: path4.relative(collection.path, realPath),
5565
5895
  templateName: args.template
5566
5896
  });
5567
5897
  }
5898
+ await this.assertAuthCollectionWrite({
5899
+ collection,
5900
+ realPath,
5901
+ op: "create"
5902
+ });
5568
5903
  const params = await this.buildObjectMutations(
5569
5904
  // @ts-ignore
5570
5905
  args.params[collection.name],
@@ -5583,6 +5918,11 @@ var Resolver = class _Resolver {
5583
5918
  isAddPendingDocument,
5584
5919
  isCollectionSpecific
5585
5920
  }) => {
5921
+ await this.assertAuthCollectionWrite({
5922
+ collection,
5923
+ realPath,
5924
+ op: "update"
5925
+ });
5586
5926
  const doc = await this.getDocument(realPath);
5587
5927
  const oldDoc = this.resolveLegacyValues(doc?._rawData || {}, collection);
5588
5928
  if (isAddPendingDocument === true) {
@@ -5893,7 +6233,7 @@ var Resolver = class _Resolver {
5893
6233
  filterExpression: {
5894
6234
  _type: "string",
5895
6235
  _list: false,
5896
- eq: id
6236
+ eq: normalizePath2(id)
5897
6237
  }
5898
6238
  }
5899
6239
  ]
@@ -5928,7 +6268,7 @@ var Resolver = class _Resolver {
5928
6268
  filterExpression: {
5929
6269
  _type: "string",
5930
6270
  _list: false,
5931
- eq: id
6271
+ eq: normalizePath2(id)
5932
6272
  }
5933
6273
  }
5934
6274
  ]
@@ -6089,130 +6429,6 @@ var resolveDateInput = (value) => {
6089
6429
  return date;
6090
6430
  };
6091
6431
 
6092
- // src/resolver/auth-fields.ts
6093
- import path4 from "path";
6094
- import { set } from "es-toolkit/compat";
6095
- async function getUserDocumentContext(tinaSchema, resolver) {
6096
- const collection = tinaSchema.getCollections().find((c) => c.isAuthCollection);
6097
- if (!collection) {
6098
- throw new Error("Auth collection not found");
6099
- }
6100
- const userFields = mapUserFields(collection, ["_rawData"]);
6101
- if (!userFields.length) {
6102
- throw new Error(`No user field found in collection ${collection.name}`);
6103
- }
6104
- if (userFields.length > 1) {
6105
- throw new Error(
6106
- `Multiple user fields found in collection ${collection.name}`
6107
- );
6108
- }
6109
- const userField = userFields[0];
6110
- const relativePath = "index.json";
6111
- const realPath = path4.join(collection.path, relativePath);
6112
- const userDoc = await resolver.getDocument(realPath);
6113
- const users = get(userDoc, userField.path);
6114
- if (!users) {
6115
- throw new Error("No users found");
6116
- }
6117
- return { collection, userField, users, userDoc, relativePath };
6118
- }
6119
- function findUserInCollection(users, userField, userSub) {
6120
- const { idFieldName } = userField;
6121
- if (!idFieldName) {
6122
- throw new Error("No uid field found on user field");
6123
- }
6124
- return users.find((u) => u[idFieldName] === userSub) || null;
6125
- }
6126
- async function handleAuthenticate({
6127
- tinaSchema,
6128
- resolver,
6129
- sub,
6130
- password,
6131
- ctxUser
6132
- }) {
6133
- const userSub = sub || ctxUser?.sub;
6134
- const { userField, users } = await getUserDocumentContext(
6135
- tinaSchema,
6136
- resolver
6137
- );
6138
- const user = findUserInCollection(users, userField, userSub);
6139
- if (!user) {
6140
- return null;
6141
- }
6142
- const { passwordFieldName } = userField;
6143
- const saltedHash = get(user, [passwordFieldName || "", "value"]);
6144
- if (!saltedHash) {
6145
- throw new Error("No password field found on user field");
6146
- }
6147
- const matches = await checkPasswordHash({
6148
- saltedHash,
6149
- password
6150
- });
6151
- return matches ? user : null;
6152
- }
6153
- async function handleAuthorize({
6154
- tinaSchema,
6155
- resolver,
6156
- sub,
6157
- ctxUser
6158
- }) {
6159
- const userSub = sub || ctxUser?.sub;
6160
- const { userField, users } = await getUserDocumentContext(
6161
- tinaSchema,
6162
- resolver
6163
- );
6164
- const user = findUserInCollection(users, userField, userSub);
6165
- return user ? user : null;
6166
- }
6167
- async function handleUpdatePassword({
6168
- tinaSchema,
6169
- resolver,
6170
- password,
6171
- ctxUser
6172
- }) {
6173
- if (!ctxUser?.sub) {
6174
- throw new Error("Not authorized");
6175
- }
6176
- if (!password) {
6177
- throw new Error("No password provided");
6178
- }
6179
- const { collection, userField, users, relativePath } = await getUserDocumentContext(tinaSchema, resolver);
6180
- const { idFieldName, passwordFieldName } = userField;
6181
- const user = users.find((u) => u[idFieldName] === ctxUser.sub);
6182
- if (!user) {
6183
- throw new Error("Not authorized");
6184
- }
6185
- user[passwordFieldName] = {
6186
- value: password,
6187
- passwordChangeRequired: false
6188
- };
6189
- const newBody = {};
6190
- set(
6191
- newBody,
6192
- userField.path.slice(1),
6193
- // remove _rawData from users path
6194
- users.map((u) => {
6195
- if (user[idFieldName] === u[idFieldName]) {
6196
- return user;
6197
- }
6198
- return {
6199
- // don't overwrite other users' passwords
6200
- ...u,
6201
- [passwordFieldName]: {
6202
- ...u[passwordFieldName],
6203
- value: ""
6204
- }
6205
- };
6206
- })
6207
- );
6208
- await resolver.resolveUpdateDocument({
6209
- collectionName: collection.name,
6210
- relativePath,
6211
- newBody
6212
- });
6213
- return true;
6214
- }
6215
-
6216
6432
  // src/error.ts
6217
6433
  import { GraphQLError as GraphQLError3 } from "graphql";
6218
6434
  var NotFoundError = class extends GraphQLError3 {
@@ -6231,7 +6447,8 @@ var resolve = async ({
6231
6447
  silenceErrors,
6232
6448
  verbose,
6233
6449
  isAudit,
6234
- ctxUser
6450
+ ctxUser,
6451
+ isSignIn
6235
6452
  }) => {
6236
6453
  try {
6237
6454
  const verboseValue = verbose ?? true;
@@ -6250,7 +6467,8 @@ var resolve = async ({
6250
6467
  config,
6251
6468
  database,
6252
6469
  tinaSchema,
6253
- isAudit: isAudit || false
6470
+ isAudit: isAudit || false,
6471
+ ctxUser
6254
6472
  });
6255
6473
  const resolverPromises = [];
6256
6474
  const res = await graphql({
@@ -6294,7 +6512,7 @@ var resolve = async ({
6294
6512
  sub: authArgs.sub,
6295
6513
  password: authArgs.password,
6296
6514
  info,
6297
- ctxUser
6515
+ isSignIn
6298
6516
  });
6299
6517
  }
6300
6518
  if (info.fieldName === "authorize") {
@@ -6660,7 +6878,11 @@ import path5 from "node:path";
6660
6878
  import { GraphQLError as GraphQLError5 } from "graphql";
6661
6879
  import micromatch2 from "micromatch";
6662
6880
  import sha2 from "js-sha1";
6663
- import { set as set2 } from "es-toolkit/compat";
6881
+ import { set } from "es-toolkit/compat";
6882
+ var withUnauthenticatedWrites = (options) => ({
6883
+ ...options,
6884
+ allowUnauthenticatedWrites: options?.allowUnauthenticatedWrites ?? true
6885
+ });
6664
6886
  var createLocalDatabase = (config) => {
6665
6887
  const level = new TinaLevelClient(config?.port);
6666
6888
  level.openConnection();
@@ -6668,7 +6890,8 @@ var createLocalDatabase = (config) => {
6668
6890
  return new Database({
6669
6891
  bridge: fsBridge,
6670
6892
  ...config || {},
6671
- level
6893
+ level,
6894
+ authCollection: withUnauthenticatedWrites(config?.authCollection)
6672
6895
  });
6673
6896
  };
6674
6897
  var createDatabase = (config) => {
@@ -6709,7 +6932,8 @@ var createDatabaseInternal = (config) => {
6709
6932
  return new Database({
6710
6933
  ...config,
6711
6934
  bridge: config.bridge,
6712
- level: config.level
6935
+ level: config.level,
6936
+ authCollection: withUnauthenticatedWrites(config.authCollection)
6713
6937
  });
6714
6938
  };
6715
6939
  var SYSTEM_FILES = ["_schema", "_graphql", "_lookup"];
@@ -6728,6 +6952,10 @@ var Database = class {
6728
6952
  this.onDelete = config.onDelete || defaultOnDelete;
6729
6953
  this.levelBatchSize = config.levelBatchSize ?? DEFAULT_LEVEL_BATCH_SIZE;
6730
6954
  this.contentNamespace = config.namespace;
6955
+ this.authCollection = Object.freeze({
6956
+ admins: Object.freeze([...config.authCollection?.admins ?? []]),
6957
+ allowUnauthenticatedWrites: config.authCollection?.allowUnauthenticatedWrites === true
6958
+ });
6731
6959
  }
6732
6960
  config;
6733
6961
  bridge;
@@ -6739,6 +6967,7 @@ var Database = class {
6739
6967
  onPut;
6740
6968
  onDelete;
6741
6969
  levelBatchSize;
6970
+ authCollection;
6742
6971
  tinaSchema;
6743
6972
  contentNamespace;
6744
6973
  collectionIndexDefinitions;
@@ -7335,7 +7564,7 @@ var Database = class {
7335
7564
  }
7336
7565
  if (fields) {
7337
7566
  for (const field of fields) {
7338
- if (field.indexed !== void 0 && field.indexed === false || field.type === "object") {
7567
+ if (field.indexed !== void 0 && field.indexed === false || field.type === "password" || field.type === "object") {
7339
7568
  continue;
7340
7569
  }
7341
7570
  indexDefinitions[field.name] = {
@@ -7800,7 +8029,7 @@ var hashPasswordVisitor = async (node, path9) => {
7800
8029
  const passwordValuePath = [...path9, "value"];
7801
8030
  const plaintextPassword = get(node, passwordValuePath);
7802
8031
  if (plaintextPassword) {
7803
- set2(
8032
+ set(
7804
8033
  node,
7805
8034
  passwordValuePath,
7806
8035
  await generatePasswordHash({ password: plaintextPassword })
package/dist/resolve.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { GraphQLConfig } from './types';
2
2
  import type { Database } from './database';
3
- export declare const resolve: ({ config, query, variables, database, silenceErrors, verbose, isAudit, ctxUser, }: {
3
+ import { type CtxUser } from './resolver';
4
+ export declare const resolve: ({ config, query, variables, database, silenceErrors, verbose, isAudit, ctxUser, isSignIn, }: {
4
5
  config?: GraphQLConfig;
5
6
  query: string;
6
7
  variables: object;
@@ -8,9 +9,9 @@ export declare const resolve: ({ config, query, variables, database, silenceErro
8
9
  silenceErrors?: boolean;
9
10
  verbose?: boolean;
10
11
  isAudit?: boolean;
11
- ctxUser?: {
12
- sub: string;
13
- };
12
+ ctxUser?: CtxUser;
13
+ /** Set only by the generated database client's `authenticate()`. */
14
+ isSignIn?: boolean;
14
15
  }) => Promise<import("graphql").ExecutionResult<{
15
16
  [key: string]: any;
16
17
  }, {
@@ -1,15 +1,56 @@
1
- import type { TinaSchema } from '@tinacms/schema-tools';
1
+ import type { Collection, TinaSchema } from '@tinacms/schema-tools';
2
2
  import type { GraphQLResolveInfo } from 'graphql';
3
3
  import type { Resolver } from './index';
4
- export declare function handleAuthenticate({ tinaSchema, resolver, sub, password, ctxUser, }: {
4
+ export declare function getUserDocumentContext(tinaSchema: TinaSchema, resolver: Resolver): Promise<{
5
+ collection: Collection<true>;
6
+ userField: {
7
+ path: string[];
8
+ collectable: import("@tinacms/schema-tools").Collectable;
9
+ idFieldName?: string;
10
+ passwordFieldName?: string;
11
+ };
12
+ users: any;
13
+ userDoc: {
14
+ _sys: {
15
+ title: any;
16
+ basename: string;
17
+ filename: string;
18
+ extension: string;
19
+ hasReferences: boolean;
20
+ path: string;
21
+ relativePath: string;
22
+ breadcrumbs: string[];
23
+ collection: Collection<true>;
24
+ template: string | number;
25
+ };
26
+ _values: {
27
+ _collection: any;
28
+ _template: any;
29
+ };
30
+ _rawData: {
31
+ _collection: any;
32
+ _template: any;
33
+ };
34
+ _collection: any;
35
+ _template: any;
36
+ __typename: string;
37
+ id: string;
38
+ };
39
+ relativePath: string;
40
+ }>;
41
+ export declare function findUserInCollection(users: any[], userField: any, userSub: string): any;
42
+ /**
43
+ * A stored hash only carries over to a user with the same uid, so a new or
44
+ * renamed user saved without a password could never sign in.
45
+ */
46
+ export declare function assertNewUsersHavePasswords(collection: Collection<true>, newBody: Record<string, unknown>, existingData?: Record<string, unknown>): void;
47
+ export declare function handleAuthenticate({ tinaSchema, resolver, sub, password, isSignIn, }: {
5
48
  tinaSchema: TinaSchema;
6
49
  resolver: Resolver;
7
50
  sub?: string;
8
51
  password: string;
9
52
  info: GraphQLResolveInfo;
10
- ctxUser?: {
11
- sub?: string;
12
- };
53
+ isSignIn?: boolean;
13
54
  }): Promise<any>;
14
55
  export declare function handleAuthorize({ tinaSchema, resolver, sub, ctxUser, }: {
15
56
  tinaSchema: TinaSchema;
@@ -18,7 +59,7 @@ export declare function handleAuthorize({ tinaSchema, resolver, sub, ctxUser, }:
18
59
  info: GraphQLResolveInfo;
19
60
  ctxUser?: {
20
61
  sub?: string;
21
- };
62
+ } | null;
22
63
  }): Promise<any>;
23
64
  export declare function handleUpdatePassword({ tinaSchema, resolver, password, ctxUser, }: {
24
65
  tinaSchema: TinaSchema;
@@ -27,5 +68,5 @@ export declare function handleUpdatePassword({ tinaSchema, resolver, password, c
27
68
  info: GraphQLResolveInfo;
28
69
  ctxUser?: {
29
70
  sub?: string;
30
- };
71
+ } | null;
31
72
  }): Promise<boolean>;
@@ -4,11 +4,26 @@
4
4
  import { Database } from '../database';
5
5
  import type { Collectable, Collection, Template, TinaField, TinaSchema } from '@tinacms/schema-tools';
6
6
  import type { GraphQLConfig } from '../types';
7
+ export type AuthCollectionWriteOp = 'create' | 'update' | 'rename' | 'delete' | 'addPending' | 'createFolder';
8
+ /**
9
+ * The signed-in user behind a request. Leave it undefined only when the caller
10
+ * vouches for the call itself, and pass null when nobody is signed in.
11
+ */
12
+ export type CtxUser = {
13
+ sub?: string;
14
+ } | null | undefined;
15
+ type UpdateDocumentArgs = {
16
+ collectionName: string;
17
+ relativePath: string;
18
+ newRelativePath?: string;
19
+ newBody?: Record<string, unknown>;
20
+ };
7
21
  interface ResolverConfig {
8
22
  config?: GraphQLConfig;
9
23
  database: Database;
10
24
  tinaSchema: TinaSchema;
11
25
  isAudit: boolean;
26
+ ctxUser?: CtxUser;
12
27
  }
13
28
  export declare const createResolver: (args: ResolverConfig) => Resolver;
14
29
  export declare const resolveFieldData: ({ namespace, ...field }: TinaField<true>, rawData: unknown, accumulator: {
@@ -66,6 +81,7 @@ export declare class Resolver {
66
81
  database: Database;
67
82
  tinaSchema: TinaSchema;
68
83
  isAudit: boolean;
84
+ ctxUser: CtxUser;
69
85
  constructor(init: ResolverConfig);
70
86
  resolveCollection: (_args: any, collectionName: string, hasDocuments?: boolean) => Promise<{
71
87
  fields: TinaField<true>[];
@@ -248,6 +264,12 @@ export declare class Resolver {
248
264
  */
249
265
  private static validateRelativePath;
250
266
  private validatePath;
267
+ private assertAuthCollectionWrite;
268
+ private assertReferenceUpdates;
269
+ private isAuthCollectionPath;
270
+ private withAuthCollectionLock;
271
+ private isStoredInCollection;
272
+ private isStoredUser;
251
273
  /**
252
274
  * Helper method to get collection and construct validated path.
253
275
  * This encapsulates the common pattern of getting a collection, joining paths,
@@ -256,7 +278,9 @@ export declare class Resolver {
256
278
  * @param collectionName - Name of the collection
257
279
  * @param relativePath - Relative path within the collection
258
280
  * @param options - Optional configuration
259
- * @returns Object containing the collection and validated real path
281
+ * @returns Object containing the collection and the validated real path,
282
+ * normalized to POSIX separators so it matches the document identities and
283
+ * reference values stored in content files on every platform
260
284
  */
261
285
  private getValidatedPath;
262
286
  resolveRetrievedDocument: ({ collectionName, relativePath, }: {
@@ -347,12 +371,7 @@ export declare class Resolver {
347
371
  __typename: string;
348
372
  id: string;
349
373
  }>;
350
- resolveUpdateDocument: ({ collectionName, relativePath, newRelativePath, newBody, }: {
351
- collectionName: string;
352
- relativePath: string;
353
- newRelativePath?: string;
354
- newBody?: Record<string, unknown>;
355
- }) => Promise<{
374
+ resolveUpdateDocument: (args: UpdateDocumentArgs) => Promise<{
356
375
  _sys: {
357
376
  title: any;
358
377
  basename: string;
@@ -378,6 +397,12 @@ export declare class Resolver {
378
397
  __typename: string;
379
398
  id: string;
380
399
  }>;
400
+ private updateDocument;
401
+ /**
402
+ * Rewrites the auth collection's user store without the admin check, so the
403
+ * caller must check the request user.
404
+ */
405
+ updateAuthDocumentInternal: (update: (rawData: Record<string, any>) => Record<string, any>) => Promise<void>;
381
406
  resolveDeleteDocument: ({ collectionName, relativePath, }: {
382
407
  collectionName: string;
383
408
  relativePath: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tinacms/graphql",
3
3
  "type": "module",
4
- "version": "2.4.11",
4
+ "version": "4.0.0",
5
5
  "main": "dist/index.js",
6
6
  "module": "./dist/index.js",
7
7
  "files": [
@@ -43,8 +43,8 @@
43
43
  "normalize-path": "^3.0.0",
44
44
  "readable-stream": "^4.7.0",
45
45
  "yup": "^1.6.1",
46
- "@tinacms/mdx": "^2.2.2",
47
- "@tinacms/schema-tools": "^2.10.0"
46
+ "@tinacms/schema-tools": "^2.10.1",
47
+ "@tinacms/mdx": "^2.3.0"
48
48
  },
49
49
  "publishConfig": {
50
50
  "registry": "https://registry.npmjs.org"
@@ -72,7 +72,7 @@
72
72
  "vite": "^4.5.9",
73
73
  "vitest": "^0.32.4",
74
74
  "zod": "^3.24.2",
75
- "@tinacms/schema-tools": "2.10.0",
75
+ "@tinacms/schema-tools": "2.10.1",
76
76
  "@tinacms/scripts": "1.6.4"
77
77
  },
78
78
  "scripts": {